Skip to content

Commit 1949d41

Browse files
feat(powerpoint): ✨ Add commands to create shapes and tables in PowerPoint slides
- Introduced `Add-OfficePowerPointShape` to add basic shapes with customizable properties. - Added `Add-OfficePowerPointTable` for creating tables from data or fixed sizes. - Removed `ExcelHostExtensions` as it is no longer needed.
1 parent a6d4dd3 commit 1949d41

File tree

6 files changed

+359
-78
lines changed

6 files changed

+359
-78
lines changed

Sources/PSWriteOffice/Cmdlets/Excel/SetOfficeExcelCellCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Management.Automation;
2+
using OfficeIMO.Excel;
23
using PSWriteOffice.Services.Excel;
34

45
namespace PSWriteOffice.Cmdlets.Excel;

Sources/PSWriteOffice/Cmdlets/Excel/SetOfficeExcelColumnCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Management.Automation;
4+
using OfficeIMO.Excel;
45
using PSWriteOffice.Services.Excel;
56

67
namespace PSWriteOffice.Cmdlets.Excel;

Sources/PSWriteOffice/Cmdlets/Excel/SetOfficeExcelFormulaCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Management.Automation;
2+
using OfficeIMO.Excel;
23
using PSWriteOffice.Services.Excel;
34

45
namespace PSWriteOffice.Cmdlets.Excel;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Management.Automation;
3+
using System.Reflection;
4+
using DocumentFormat.OpenXml.Drawing;
5+
using OfficeIMO.PowerPoint;
6+
using SixLabors.ImageSharp;
7+
8+
namespace PSWriteOffice.Cmdlets.PowerPoint;
9+
10+
/// <summary>Adds a basic shape to a slide.</summary>
11+
/// <para>Creates an auto shape at the requested coordinates and applies optional fill and outline styling.</para>
12+
/// <example>
13+
/// <summary>Create a rectangle highlight.</summary>
14+
/// <prefix>PS&gt; </prefix>
15+
/// <code>Add-OfficePowerPointShape -Slide $slide -ShapeType Rectangle -X 60 -Y 80 -Width 220 -Height 120 -FillColor '#DDEEFF'</code>
16+
/// <para>Creates a rectangle with a custom fill color.</para>
17+
/// </example>
18+
[Cmdlet(VerbsCommon.Add, "OfficePowerPointShape")]
19+
public sealed class AddOfficePowerPointShapeCommand : PSCmdlet
20+
{
21+
/// <summary>Target slide that will receive the shape.</summary>
22+
[Parameter(Mandatory = true, ValueFromPipeline = true)]
23+
public PowerPointSlide Slide { get; set; } = null!;
24+
25+
/// <summary>Shape geometry preset name (e.g., Rectangle, Ellipse, Line).</summary>
26+
[Parameter]
27+
public string ShapeType { get; set; } = "Rectangle";
28+
29+
/// <summary>Left offset (in points) from the slide origin.</summary>
30+
[Parameter]
31+
public double X { get; set; } = 50;
32+
33+
/// <summary>Top offset (in points) from the slide origin.</summary>
34+
[Parameter]
35+
public double Y { get; set; } = 50;
36+
37+
/// <summary>Shape width in points.</summary>
38+
[Parameter]
39+
public double Width { get; set; } = 200;
40+
41+
/// <summary>Shape height in points.</summary>
42+
[Parameter]
43+
public double Height { get; set; } = 100;
44+
45+
/// <summary>Optional name assigned to the shape.</summary>
46+
[Parameter]
47+
public string? Name { get; set; }
48+
49+
/// <summary>Fill color (hex or named color).</summary>
50+
[Parameter]
51+
public string? FillColor { get; set; }
52+
53+
/// <summary>Outline color (hex or named color).</summary>
54+
[Parameter]
55+
public string? OutlineColor { get; set; }
56+
57+
/// <summary>Outline width in points.</summary>
58+
[Parameter]
59+
public double? OutlineWidth { get; set; }
60+
61+
/// <inheritdoc />
62+
protected override void ProcessRecord()
63+
{
64+
try
65+
{
66+
if (Width <= 0)
67+
{
68+
throw new ArgumentOutOfRangeException(nameof(Width), "Width must be greater than 0.");
69+
}
70+
71+
if (Height <= 0)
72+
{
73+
throw new ArgumentOutOfRangeException(nameof(Height), "Height must be greater than 0.");
74+
}
75+
76+
if (OutlineWidth is < 0)
77+
{
78+
throw new ArgumentOutOfRangeException(nameof(OutlineWidth), "OutlineWidth cannot be negative.");
79+
}
80+
81+
var shapeType = ResolveShapeType(ShapeType);
82+
var shape = Slide.AddShapePoints(shapeType, X, Y, Width, Height, Name);
83+
84+
var fill = NormalizeColor(FillColor);
85+
if (fill != null)
86+
{
87+
shape.FillColor = fill;
88+
}
89+
90+
var outline = NormalizeColor(OutlineColor);
91+
if (outline != null)
92+
{
93+
shape.OutlineColor = outline;
94+
}
95+
96+
if (OutlineWidth.HasValue)
97+
{
98+
shape.OutlineWidthPoints = OutlineWidth.Value;
99+
}
100+
101+
WriteObject(shape);
102+
}
103+
catch (Exception ex)
104+
{
105+
WriteError(new ErrorRecord(ex, "PowerPointAddShapeFailed", ErrorCategory.InvalidOperation, Slide));
106+
}
107+
}
108+
109+
private static string? NormalizeColor(string? color)
110+
{
111+
if (string.IsNullOrWhiteSpace(color))
112+
{
113+
return null;
114+
}
115+
116+
var parsed = Color.Parse(color);
117+
var hex = parsed.ToHex().ToLowerInvariant();
118+
return hex.Length > 6 ? hex.Substring(0, 6) : hex;
119+
}
120+
121+
private static ShapeTypeValues ResolveShapeType(string? shapeType)
122+
{
123+
if (string.IsNullOrWhiteSpace(shapeType))
124+
{
125+
return ShapeTypeValues.Rectangle;
126+
}
127+
128+
var property = typeof(ShapeTypeValues).GetProperty(
129+
shapeType,
130+
BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase);
131+
132+
if (property == null)
133+
{
134+
throw new PSArgumentException($"Unknown shape type '{shapeType}'.", nameof(ShapeType));
135+
}
136+
137+
return (ShapeTypeValues)property.GetValue(null)!;
138+
}
139+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.Linq;
6+
using System.Management.Automation;
7+
using OfficeIMO.PowerPoint;
8+
using PSWriteOffice.Services;
9+
10+
namespace PSWriteOffice.Cmdlets.PowerPoint;
11+
12+
/// <summary>Adds a table to a PowerPoint slide.</summary>
13+
/// <para>Builds a table from data rows or creates a blank grid with a fixed size.</para>
14+
/// <example>
15+
/// <summary>Create a table from objects.</summary>
16+
/// <prefix>PS&gt; </prefix>
17+
/// <code>$rows = @([pscustomobject]@{ Item='Alpha'; Qty=2 }, [pscustomobject]@{ Item='Beta'; Qty=4 })
18+
/// Add-OfficePowerPointTable -Slide $slide -Data $rows -X 60 -Y 140 -Width 420 -Height 200</code>
19+
/// <para>Creates a table with headers and two data rows.</para>
20+
/// </example>
21+
[Cmdlet(VerbsCommon.Add, "OfficePowerPointTable", DefaultParameterSetName = ParameterSetData)]
22+
public sealed class AddOfficePowerPointTableCommand : PSCmdlet
23+
{
24+
private const string ParameterSetData = "Data";
25+
private const string ParameterSetSize = "Size";
26+
27+
/// <summary>Target slide that will receive the table.</summary>
28+
[Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
29+
public PowerPointSlide Slide { get; set; } = null!;
30+
31+
/// <summary>Source objects to convert into table rows.</summary>
32+
[Parameter(Mandatory = true, ParameterSetName = ParameterSetData)]
33+
public object[] Data { get; set; } = Array.Empty<object>();
34+
35+
/// <summary>Optional header order to apply to the table.</summary>
36+
[Parameter(ParameterSetName = ParameterSetData)]
37+
public string[]? Headers { get; set; }
38+
39+
/// <summary>Skip writing header row.</summary>
40+
[Parameter(ParameterSetName = ParameterSetData)]
41+
public SwitchParameter NoHeader { get; set; }
42+
43+
/// <summary>Row count for an empty table.</summary>
44+
[Parameter(Mandatory = true, ParameterSetName = ParameterSetSize)]
45+
public int Rows { get; set; }
46+
47+
/// <summary>Column count for an empty table.</summary>
48+
[Parameter(Mandatory = true, ParameterSetName = ParameterSetSize)]
49+
public int Columns { get; set; }
50+
51+
/// <summary>Left offset (in points) from the slide origin.</summary>
52+
[Parameter]
53+
public double X { get; set; } = 0;
54+
55+
/// <summary>Top offset (in points) from the slide origin.</summary>
56+
[Parameter]
57+
public double Y { get; set; } = 0;
58+
59+
/// <summary>Table width in points.</summary>
60+
[Parameter]
61+
public double Width { get; set; } = 400;
62+
63+
/// <summary>Table height in points.</summary>
64+
[Parameter]
65+
public double Height { get; set; } = 240;
66+
67+
/// <summary>Optional table style ID (GUID string).</summary>
68+
[Parameter]
69+
public string? StyleId { get; set; }
70+
71+
/// <inheritdoc />
72+
protected override void ProcessRecord()
73+
{
74+
try
75+
{
76+
ValidateDimensions();
77+
78+
PowerPointTable table = ParameterSetName == ParameterSetSize
79+
? CreateSizedTable()
80+
: CreateDataTable();
81+
82+
if (!string.IsNullOrWhiteSpace(StyleId))
83+
{
84+
table.StyleId = StyleId;
85+
}
86+
87+
WriteObject(table);
88+
}
89+
catch (Exception ex)
90+
{
91+
WriteError(new ErrorRecord(ex, "PowerPointAddTableFailed", ErrorCategory.InvalidOperation, Slide));
92+
}
93+
}
94+
95+
private PowerPointTable CreateSizedTable()
96+
{
97+
if (Rows <= 0)
98+
{
99+
throw new ArgumentOutOfRangeException(nameof(Rows), "Rows must be greater than 0.");
100+
}
101+
102+
if (Columns <= 0)
103+
{
104+
throw new ArgumentOutOfRangeException(nameof(Columns), "Columns must be greater than 0.");
105+
}
106+
107+
return Slide.AddTablePoints(Rows, Columns, X, Y, Width, Height);
108+
}
109+
110+
private PowerPointTable CreateDataTable()
111+
{
112+
if (Data == null || Data.Length == 0)
113+
{
114+
throw new PSArgumentException("Provide at least one data row.", nameof(Data));
115+
}
116+
117+
var normalized = PowerShellObjectNormalizer.NormalizeItems(Data);
118+
var rows = NormalizeRows(normalized);
119+
var headers = ResolveHeaders(rows);
120+
121+
if (headers.Count == 0)
122+
{
123+
throw new InvalidOperationException("Unable to infer columns from the supplied data.");
124+
}
125+
126+
var columns = headers
127+
.Select(header => PowerPointTableColumn<Dictionary<string, object?>>.Create(
128+
header,
129+
row => row.TryGetValue(header, out var value) ? value : null))
130+
.ToList();
131+
132+
return Slide.AddTablePoints(rows, columns, includeHeaders: !NoHeader.IsPresent, X, Y, Width, Height);
133+
}
134+
135+
private void ValidateDimensions()
136+
{
137+
if (Width <= 0)
138+
{
139+
throw new ArgumentOutOfRangeException(nameof(Width), "Width must be greater than 0.");
140+
}
141+
142+
if (Height <= 0)
143+
{
144+
throw new ArgumentOutOfRangeException(nameof(Height), "Height must be greater than 0.");
145+
}
146+
}
147+
148+
private List<Dictionary<string, object?>> NormalizeRows(IReadOnlyList<object?> items)
149+
{
150+
var rows = new List<Dictionary<string, object?>>(items.Count);
151+
foreach (var item in items)
152+
{
153+
if (item == null)
154+
{
155+
rows.Add(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase));
156+
continue;
157+
}
158+
159+
if (item is IDictionary dict)
160+
{
161+
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
162+
foreach (DictionaryEntry entry in dict)
163+
{
164+
var key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
165+
if (string.IsNullOrWhiteSpace(key))
166+
{
167+
continue;
168+
}
169+
row[key] = entry.Value;
170+
}
171+
172+
rows.Add(row);
173+
continue;
174+
}
175+
176+
rows.Add(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
177+
{
178+
["Value"] = item
179+
});
180+
}
181+
182+
return rows;
183+
}
184+
185+
private List<string> ResolveHeaders(IReadOnlyList<Dictionary<string, object?>> rows)
186+
{
187+
if (Headers != null && Headers.Length > 0)
188+
{
189+
var explicitHeaders = Headers
190+
.Where(h => !string.IsNullOrWhiteSpace(h))
191+
.Distinct(StringComparer.OrdinalIgnoreCase)
192+
.ToList();
193+
194+
if (explicitHeaders.Count == 0)
195+
{
196+
throw new PSArgumentException("Headers cannot be empty.", nameof(Headers));
197+
}
198+
199+
return explicitHeaders;
200+
}
201+
202+
var headers = new List<string>();
203+
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
204+
foreach (var row in rows)
205+
{
206+
foreach (var key in row.Keys)
207+
{
208+
if (seen.Add(key))
209+
{
210+
headers.Add(key);
211+
}
212+
}
213+
}
214+
215+
return headers;
216+
}
217+
}

0 commit comments

Comments
 (0)