mirror of
https://github.com/iOfficeAI/OfficeCLI
synced 2026-04-21 13:37:23 +00:00
feat(pptx): add placeholder via CLI and preserve chExt on group resize
Two CLI gaps carried from earlier rounds. AddPlaceholder: `add /slide[N] --type placeholder --prop phType=...` now creates a <p:sp> with <p:ph type="..."/> for any of the known placeholder types (body, date, footer, slidenum, header, subtitle, title, picture, chart, table, diagram, media, obj, clipart). Emits an empty <p:spPr> so geometry and fonts inherit from the layout, plus a minimal <p:txBody> that optionally prepopulates via a text= property. Dispatch wired in Add.cs. Group resize preserves scaling: when `set /slide[N]/group[M] width=` or `height=` / `x=` / `y=` is applied and the group's `TransformGroup.ChildExtents` / `ChildOffset` is null, snapshot the current Extents / Offset into new ChildExtents / ChildOffset BEFORE updating Extents / Offset. This establishes the ext≠chExt baseline that PowerPoint needs to visibly compress the group's children instead of resizing them 1:1 with the container.
This commit is contained in:
parent
7284a748f7
commit
46cfd95a4f
3 changed files with 92 additions and 3 deletions
|
|
@ -275,6 +275,78 @@ public partial class PowerPointHandler
|
|||
}
|
||||
|
||||
|
||||
// CONSISTENCY(add-dispatch-shape): mirrors AddGroup/AddShape resolution flow.
|
||||
// Emits a <p:sp> with <p:ph type="..."/> that binds to the layout's matching
|
||||
// placeholder. Leaves <p:spPr> empty so PowerPoint inherits geometry/font
|
||||
// from the layout placeholder. Optional --prop text=... prepopulates text.
|
||||
private string AddPlaceholder(string parentPath, int? index, Dictionary<string, string> properties)
|
||||
{
|
||||
var phSlideMatch = Regex.Match(parentPath, @"^/slide\[(\d+)\]$");
|
||||
if (!phSlideMatch.Success)
|
||||
throw new ArgumentException("Placeholders must be added to a slide: /slide[N]");
|
||||
|
||||
var phSlideIdx = int.Parse(phSlideMatch.Groups[1].Value);
|
||||
var phSlideParts = GetSlideParts().ToList();
|
||||
if (phSlideIdx < 1 || phSlideIdx > phSlideParts.Count)
|
||||
throw new ArgumentException($"Slide {phSlideIdx} not found (total: {phSlideParts.Count})");
|
||||
|
||||
var phSlidePart = phSlideParts[phSlideIdx - 1];
|
||||
var phShapeTree = GetSlide(phSlidePart).CommonSlideData?.ShapeTree
|
||||
?? throw new InvalidOperationException("Slide has no shape tree");
|
||||
|
||||
if (!properties.TryGetValue("phType", out var phTypeStr)
|
||||
&& !properties.TryGetValue("phtype", out phTypeStr)
|
||||
&& !properties.TryGetValue("type", out phTypeStr))
|
||||
throw new ArgumentException("'phType' property required for placeholder type (e.g. phType=body|date|footer|slidenum|header|subtitle|title)");
|
||||
|
||||
var phTypeVal = ParsePlaceholderType(phTypeStr)
|
||||
?? throw new ArgumentException(
|
||||
$"Invalid placeholder type: '{phTypeStr}'. Valid: title, body, subtitle, date, footer, slidenum, header, picture, chart, table, diagram, media, obj, clipart.");
|
||||
|
||||
var phId = GenerateUniqueShapeId(phShapeTree);
|
||||
var phName = properties.GetValueOrDefault("name", $"{phTypeStr} Placeholder {phId}");
|
||||
|
||||
var shape = new Shape();
|
||||
var appNvPr = new ApplicationNonVisualDrawingProperties();
|
||||
appNvPr.AppendChild(new PlaceholderShape { Type = phTypeVal });
|
||||
shape.NonVisualShapeProperties = new NonVisualShapeProperties(
|
||||
new NonVisualDrawingProperties { Id = phId, Name = phName },
|
||||
new NonVisualShapeDrawingProperties(),
|
||||
appNvPr
|
||||
);
|
||||
// Leave ShapeProperties empty — PowerPoint pulls geometry from layout.
|
||||
shape.ShapeProperties = new ShapeProperties();
|
||||
|
||||
// Optional text prepopulation. Build a minimal TextBody so PowerPoint
|
||||
// still renders layout placeholder typography.
|
||||
var textBody = new TextBody(
|
||||
new Drawing.BodyProperties(),
|
||||
new Drawing.ListStyle()
|
||||
);
|
||||
var para = new Drawing.Paragraph();
|
||||
if (properties.TryGetValue("text", out var phText) && phText.Length > 0)
|
||||
{
|
||||
para.AppendChild(new Drawing.Run(
|
||||
new Drawing.RunProperties { Language = "en-US" },
|
||||
new Drawing.Text(phText)
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Empty paragraph is valid — PowerPoint shows the layout prompt text.
|
||||
para.AppendChild(new Drawing.EndParagraphRunProperties { Language = "en-US" });
|
||||
}
|
||||
textBody.AppendChild(para);
|
||||
shape.TextBody = textBody;
|
||||
|
||||
InsertAtPosition(phShapeTree, shape, index);
|
||||
GetSlide(phSlidePart).Save();
|
||||
|
||||
var shapeCount = phShapeTree.Elements<Shape>().Count();
|
||||
return $"/slide[{phSlideIdx}]/shape[{shapeCount}]";
|
||||
}
|
||||
|
||||
|
||||
private string AddAnimation(string parentPath, int? index, Dictionary<string, string> properties)
|
||||
{
|
||||
// Add animation to a shape: parentPath must be /slide[N]/shape[M]
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public partial class PowerPointHandler
|
|||
"video" or "audio" or "media" => AddMedia(parentPath, index, properties, type),
|
||||
"connector" or "connection" => AddConnector(parentPath, index, properties),
|
||||
"group" => AddGroup(parentPath, index, properties),
|
||||
"placeholder" or "ph" => AddPlaceholder(parentPath, index, properties),
|
||||
"row" or "tr" => AddRow(parentPath, index, properties),
|
||||
"col" or "column" => AddColumn(parentPath, index, properties),
|
||||
"cell" or "tc" => AddCell(parentPath, index, properties),
|
||||
|
|
|
|||
|
|
@ -1956,9 +1956,25 @@ public partial class PowerPointHandler
|
|||
{
|
||||
var grpSpPr = grp.GroupShapeProperties ?? (grp.GroupShapeProperties = new GroupShapeProperties());
|
||||
var xfrm = grpSpPr.TransformGroup ?? (grpSpPr.TransformGroup = new Drawing.TransformGroup());
|
||||
TryApplyPositionSize(key.ToLowerInvariant(), value,
|
||||
xfrm.Offset ?? (xfrm.Offset = new Drawing.Offset()),
|
||||
xfrm.Extents ?? (xfrm.Extents = new Drawing.Extents()));
|
||||
var off = xfrm.Offset ?? (xfrm.Offset = new Drawing.Offset());
|
||||
var ext = xfrm.Extents ?? (xfrm.Extents = new Drawing.Extents());
|
||||
var keyLower = key.ToLowerInvariant();
|
||||
// CONSISTENCY(group-scale-baseline): group scaling needs <a:chOff>/<a:chExt>
|
||||
// as a child-coordinate baseline. Before we mutate ext/off, snapshot the
|
||||
// current ext/off into chExt/chOff if they aren't already present — that
|
||||
// way the first Set of width/height captures the "before" as the logical
|
||||
// child coordinate space, so shrinking ext shrinks the rendered children.
|
||||
if (keyLower is "x" or "y")
|
||||
{
|
||||
if (xfrm.ChildOffset == null)
|
||||
xfrm.ChildOffset = new Drawing.ChildOffset { X = off.X ?? 0, Y = off.Y ?? 0 };
|
||||
}
|
||||
else // width or height
|
||||
{
|
||||
if (xfrm.ChildExtents == null)
|
||||
xfrm.ChildExtents = new Drawing.ChildExtents { Cx = ext.Cx ?? 0, Cy = ext.Cy ?? 0 };
|
||||
}
|
||||
TryApplyPositionSize(keyLower, value, off, ext);
|
||||
break;
|
||||
}
|
||||
case "rotation" or "rotate":
|
||||
|
|
|
|||
Loading…
Reference in a new issue