src/report/renderers/Export-Pptx.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Auto-assemble an executive PowerPoint deck from scored findings via the OpenXML SDK (DocumentFormat.OpenXml) — no Python, no PowerPoint install. .DESCRIPTION Renders the Tier-3 executive deck directly from the scored Findings object (the output of Get-Score: GeneratedOn/Frameworks/Areas/Gaps/Manual/Errors/ Findings). Every element of the .pptx OPC package (theme, slide master, slide layouts, slides, tables) is constructed programmatically with the OpenXML SDK — this is the accepted design in docs/design/decisions/pptx-renderer.md (AB#5044). Slide inventory: 1. Title — deck branding, generated date, scope/mgmt-group if known 2. Executive Summary — CAF/WAF score cards + rollup counts 3..N Area Breakdown — Framework/Area/Score/Pass/Partial/Fail/Manual table, paginated at 10 rows/slide N+1.. Prioritized Gaps — Severity/Area/Gap table, sorted worst-first, paginated at 10 rows/slide, capped at top 15 .. Manual Review — outstanding manual-review worklist (capped at 12 rows) .. Next Steps — closing recommendations TEMPLATE-AS-CODE EXTENSION POINT: the theme/master/layout/chrome built by New-ScoutDeckShell and Add-ScoutSlideChrome below is the programmatic stand-in for a designer-authored deck.pptx.template (see the decision record's amendment). A future iteration can replace this function pair with code that opens a designer-authored .pptx template and clones its slide master/layout parts instead of building them from scratch here — every other function in this file (the slide-content builders) is unaffected by that swap, since they only depend on the shell exposing a content SlideLayoutPart + title SlideLayoutPart relationship ids. .NOTES Tracks ADO Story AB#5048, Epic AB#5044. Carries forward the AB#5089 defensive severity guard from the retired build_deck.py (null/unknown severity sorts LAST and never throws — see Get-ScoutSeverityRank/-Label). #> #region Assembly acquisition (first-use NuGet acquire + cache, no committed binaries) # Pinned so every run resolves the exact same OpenXML SDK build; bump deliberately. $Script:ScoutOpenXmlVersion = '3.0.2' function Import-ScoutOpenXmlAssembly { [CmdletBinding()] param() # Idempotent within a process — a prior Export-Pptx call (or Pester run) # in the same pwsh session already loaded these. $loaded = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'DocumentFormat.OpenXml' } if ($loaded) { return } $repoRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent $cacheDir = Join-Path $repoRoot 'output' '.tools' 'openxml' $Script:ScoutOpenXmlVersion $requiredDlls = @('DocumentFormat.OpenXml.Framework.dll', 'System.IO.Packaging.dll', 'DocumentFormat.OpenXml.dll') $haveAll = -not ($requiredDlls | Where-Object { -not (Test-Path (Join-Path $cacheDir $_)) }) if (-not $haveAll) { if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { throw "Export-Pptx: DocumentFormat.OpenXml $($Script:ScoutOpenXmlVersion) is not cached at '$cacheDir' and the 'dotnet' SDK is not on PATH to acquire it. Install the .NET SDK (or pre-seed the cache folder with the three DLLs above) and retry." } New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "AzScoutOpenXmlAcquire_$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null $tempProj = Join-Path $tempDir 'acquire.csproj' @" <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>disable</Nullable> <ImplicitUsings>disable</ImplicitUsings> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <EnableDefaultCompileItems>false</EnableDefaultCompileItems> </PropertyGroup> <ItemGroup> <PackageReference Include="DocumentFormat.OpenXml" Version="$($Script:ScoutOpenXmlVersion)" /> </ItemGroup> </Project> "@ | Out-File -FilePath $tempProj -Encoding utf8 Write-Host "[Export-Pptx] Acquiring DocumentFormat.OpenXml $($Script:ScoutOpenXmlVersion) via dotnet/NuGet (first use — cached under $cacheDir for subsequent runs)..." -ForegroundColor Cyan $buildOutput = & dotnet build $tempProj -c Release -o $cacheDir --nologo 2>&1 $exitCode = $LASTEXITCODE Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue Get-ChildItem $cacheDir -Filter 'acquire.*' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue if ($exitCode -ne 0 -or -not (Test-Path (Join-Path $cacheDir 'DocumentFormat.OpenXml.dll'))) { throw "Export-Pptx: could not acquire DocumentFormat.OpenXml $($Script:ScoutOpenXmlVersion) (offline, and nothing cached at '$cacheDir'?). dotnet build exit code $exitCode.`n$($buildOutput -join "`n")" } Write-Host "[Export-Pptx] DocumentFormat.OpenXml $($Script:ScoutOpenXmlVersion) cached at $cacheDir" -ForegroundColor Green } foreach ($dll in $requiredDlls) { Add-Type -Path (Join-Path $cacheDir $dll) -ErrorAction Stop } } #endregion #region Low-level OpenXML element/part helpers # # IMPORTANT: every OpenXmlElement in this SDK implements IEnumerable<OpenXmlElement> # over its own children. PowerShell's function-output pipeline auto-enumerates any # IEnumerable it sees, so a bare "return $element" silently flattens the element # into its (often zero) children instead of returning the element itself. Every # helper below returns via the unary comma operator (`return ,$x`) to suppress # that unrolling — do not remove the commas. # # For the same reason, never build an array of OpenXmlElement objects with `@(...)` # or a bare comma list — use New-ScoutList + .Add(), which is a plain .NET method # call and is not subject to pipeline enumeration. $Script:PresNs = 'DocumentFormat.OpenXml.Presentation' $Script:DrawNs = 'DocumentFormat.OpenXml.Drawing' function New-ScoutEl { param([Parameter(Mandatory)][string]$TypeName) $o = New-Object -TypeName $TypeName return , $o } function New-ScoutList { return , ([System.Collections.Generic.List[object]]::new()) } function New-ScoutPart { # Wraps the generic OpenXmlPartContainer.AddNewPart<T>(string id) method, # which PowerShell cannot call with normal method-call syntax. param($Parent, [Parameter(Mandatory)][type]$PartType, [Parameter(Mandatory)][string]$Id) $mi = $Parent.GetType().GetMethods() | Where-Object { $_.Name -eq 'AddNewPart' -and $_.IsGenericMethodDefinition -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType -eq [string] } | Select-Object -First 1 if (-not $mi) { throw "Export-Pptx: could not locate generic AddNewPart<T> on $($Parent.GetType().FullName)." } $gmi = $mi.MakeGenericMethod($PartType) $part = $gmi.Invoke($Parent, @($Id)) return , $part } function ScoutEmu { # EMU (English Metric Units), 914400 per inch. Int64 — used for a:off/a:ext, # table row heights and grid column widths (ST_PositiveCoordinate). param([double]$Inches) return [int64][math]::Round($Inches * 914400) } function ScoutEmu32 { # Int32 EMU — required specifically by p:sldSz / p:notesSz (ST_PositiveCoordinate32). param([double]$Inches) return [int32][math]::Round($Inches * 914400) } #endregion #region Fill / text / table-cell helpers function New-ScoutFill { param([Parameter(Mandatory)][string]$Hex) $fill = New-ScoutEl "$Script:DrawNs.SolidFill" $rgb = New-ScoutEl "$Script:DrawNs.RgbColorModelHex" $rgb.Val = $Hex $fill.Append($rgb) return , $fill } function New-ScoutPhColor { # Placeholder scheme-color reference used inside a theme's FormatScheme # boilerplate (fill/line/effect/background style lists). $sc = New-ScoutEl "$Script:DrawNs.SchemeColor" $sc.Val = [DocumentFormat.OpenXml.Drawing.SchemeColorValues]::PhColor return , $sc } function New-ScoutRun { param( [Parameter(Mandatory)][AllowEmptyString()][string]$Text, [double]$SizePt = 12, [string]$Hex = '1A1A1A', [bool]$Bold = $false, [string]$Font = 'Segoe UI' ) $run = New-ScoutEl "$Script:DrawNs.Run" $rPr = New-ScoutEl "$Script:DrawNs.RunProperties" $rPr.Language = 'en-US' $rPr.FontSize = [int]([math]::Round($SizePt * 100)) $rPr.Bold = $Bold $rPr.Append((New-ScoutFill $Hex)) $latin = New-ScoutEl "$Script:DrawNs.LatinFont" $latin.Typeface = $Font $rPr.Append($latin) $run.Append($rPr) $t = New-ScoutEl "$Script:DrawNs.Text" $t.Text = $Text $run.Append($t) return , $run } function New-ScoutPara { # $Runs: a List[object] of Drawing.Run elements (built with New-ScoutList/.Add()). param($Runs, [string]$Align = $null, [double]$SpaceBeforePt = 0) $p = New-ScoutEl "$Script:DrawNs.Paragraph" if ($Align -or $SpaceBeforePt -gt 0) { $pPr = New-ScoutEl "$Script:DrawNs.ParagraphProperties" if ($Align) { $pPr.Alignment = switch ($Align) { 'ctr' { [DocumentFormat.OpenXml.Drawing.TextAlignmentTypeValues]::Center } 'r' { [DocumentFormat.OpenXml.Drawing.TextAlignmentTypeValues]::Right } default { [DocumentFormat.OpenXml.Drawing.TextAlignmentTypeValues]::Left } } } if ($SpaceBeforePt -gt 0) { $spcBef = New-ScoutEl "$Script:DrawNs.SpaceBefore" $spcPts = New-ScoutEl "$Script:DrawNs.SpacingPoints" $spcPts.Val = [int]([math]::Round($SpaceBeforePt * 100)) $spcBef.Append($spcPts) $pPr.Append($spcBef) } $p.Append($pPr) } foreach ($r in $Runs) { $p.Append($r) } return , $p } function New-ScoutShapeTextBody { # $Paragraphs: a List[object] of Drawing.Paragraph — wrapped for a p:sp (shape). param($Paragraphs, [string]$Anchor = 't', [bool]$Wrap = $true) $tb = New-ScoutEl "$Script:PresNs.TextBody" $bp = New-ScoutEl "$Script:DrawNs.BodyProperties" $bp.Wrap = if ($Wrap) { [DocumentFormat.OpenXml.Drawing.TextWrappingValues]::Square } else { [DocumentFormat.OpenXml.Drawing.TextWrappingValues]::None } $bp.Anchor = switch ($Anchor) { 'ctr' { [DocumentFormat.OpenXml.Drawing.TextAnchoringTypeValues]::Center } 'b' { [DocumentFormat.OpenXml.Drawing.TextAnchoringTypeValues]::Bottom } default { [DocumentFormat.OpenXml.Drawing.TextAnchoringTypeValues]::Top } } $tb.Append($bp) $tb.Append((New-ScoutEl "$Script:DrawNs.ListStyle")) foreach ($p in $Paragraphs) { $tb.Append($p) } return , $tb } function New-ScoutCellTextBody { # $Paragraphs: a List[object] of Drawing.Paragraph — wrapped for an a:tc (table cell). param($Paragraphs) $tb = New-ScoutEl "$Script:DrawNs.TextBody" $tb.Append((New-ScoutEl "$Script:DrawNs.BodyProperties")) $tb.Append((New-ScoutEl "$Script:DrawNs.ListStyle")) foreach ($p in $Paragraphs) { $tb.Append($p) } return , $tb } #endregion #region Shape / table builders $Script:ScoutShapeId = 10 function New-ScoutShape { # A rectangle or rounded rectangle with optional fill and text content — # the one shape primitive every slide/card/label in this deck is built from. param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][double]$X, [Parameter(Mandatory)][double]$Y, [Parameter(Mandatory)][double]$Cx, [Parameter(Mandatory)][double]$Cy, [string]$FillHex = $null, $Paragraphs, [string]$Anchor = 't', [double]$RadiusPct = 0 ) $Script:ScoutShapeId++ $sh = New-ScoutEl "$Script:PresNs.Shape" $nvSp = New-ScoutEl "$Script:PresNs.NonVisualShapeProperties" $cnv = New-ScoutEl "$Script:PresNs.NonVisualDrawingProperties" $cnv.Id = [uint32]$Script:ScoutShapeId $cnv.Name = $Name $nvSp.Append($cnv) $nvSp.Append((New-ScoutEl "$Script:PresNs.NonVisualShapeDrawingProperties")) $nvSp.Append((New-ScoutEl "$Script:PresNs.ApplicationNonVisualDrawingProperties")) $sh.Append($nvSp) $spPr = New-ScoutEl "$Script:PresNs.ShapeProperties" $xfrm = New-ScoutEl "$Script:DrawNs.Transform2D" $off = New-ScoutEl "$Script:DrawNs.Offset"; $off.X = (ScoutEmu $X); $off.Y = (ScoutEmu $Y) $ext = New-ScoutEl "$Script:DrawNs.Extents"; $ext.Cx = (ScoutEmu $Cx); $ext.Cy = (ScoutEmu $Cy) $xfrm.Append($off); $xfrm.Append($ext) $spPr.Append($xfrm) $geom = New-ScoutEl "$Script:DrawNs.PresetGeometry" $geom.Preset = if ($RadiusPct -gt 0) { [DocumentFormat.OpenXml.Drawing.ShapeTypeValues]::RoundRectangle } else { [DocumentFormat.OpenXml.Drawing.ShapeTypeValues]::Rectangle } $avLst = New-ScoutEl "$Script:DrawNs.AdjustValueList" if ($RadiusPct -gt 0) { $av = New-ScoutEl "$Script:DrawNs.ShapeGuide" $av.Name = 'adj' $av.Formula = "val $([int]($RadiusPct * 1000))" $avLst.Append($av) } $geom.Append($avLst) $spPr.Append($geom) if ($FillHex) { $spPr.Append((New-ScoutFill $FillHex)) } else { $spPr.Append((New-ScoutEl "$Script:DrawNs.NoFill")) } $noLineOutline = New-ScoutEl "$Script:DrawNs.Outline" $noLineOutline.Append((New-ScoutEl "$Script:DrawNs.NoFill")) $spPr.Append($noLineOutline) $sh.Append($spPr) if ($Paragraphs) { $sh.Append((New-ScoutShapeTextBody -Paragraphs $Paragraphs -Anchor $Anchor)) } return , $sh } function New-ScoutTableCell { param( [Parameter(Mandatory)][AllowEmptyString()][string]$Text, [double]$SizePt = 11, [string]$Hex = '1A1A1A', [bool]$Bold = $false, [string]$FillHex = 'FFFFFF', [string]$Align = $null ) $tc = New-ScoutEl "$Script:DrawNs.TableCell" $runs = New-ScoutList $runs.Add((New-ScoutRun -Text $Text -SizePt $SizePt -Hex $Hex -Bold $Bold)) $paras = New-ScoutList $paras.Add((New-ScoutPara -Runs $runs -Align $Align)) $tc.Append((New-ScoutCellTextBody $paras)) $tcPr = New-ScoutEl "$Script:DrawNs.TableCellProperties" $tcPr.Anchor = [DocumentFormat.OpenXml.Drawing.TextAnchoringTypeValues]::Center $tcPr.LeftMargin = [int32](ScoutEmu 0.08) $tcPr.RightMargin = [int32](ScoutEmu 0.08) $tcPr.TopMargin = [int32](ScoutEmu 0.02) $tcPr.BottomMargin = [int32](ScoutEmu 0.02) if ($FillHex) { $tcPr.Append((New-ScoutFill $FillHex)) } $tc.Append($tcPr) return , $tc } function New-ScoutTableRow { param([double]$HeightIn, $Cells) $row = New-ScoutEl "$Script:DrawNs.TableRow" $row.Height = (ScoutEmu $HeightIn) foreach ($c in $Cells) { $row.Append($c) } return , $row } function New-ScoutTable { # $ColWidthsIn: double[] of column widths in inches. # $Rows: List[object] of Drawing.TableRow (header row typically at index 0). param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][double]$X, [Parameter(Mandatory)][double]$Y, [Parameter(Mandatory)][double[]]$ColWidthsIn, [Parameter(Mandatory)]$Rows ) $Script:ScoutShapeId++ $gf = New-ScoutEl "$Script:PresNs.GraphicFrame" $nvGfp = New-ScoutEl "$Script:PresNs.NonVisualGraphicFrameProperties" $cnv = New-ScoutEl "$Script:PresNs.NonVisualDrawingProperties" $cnv.Id = [uint32]$Script:ScoutShapeId $cnv.Name = $Name $nvGfp.Append($cnv) $locks = New-ScoutEl "$Script:DrawNs.GraphicFrameLocks"; $locks.NoGrouping = $true $nvGfdp = New-ScoutEl "$Script:PresNs.NonVisualGraphicFrameDrawingProperties" $nvGfdp.Append($locks) $nvGfp.Append($nvGfdp) $nvGfp.Append((New-ScoutEl "$Script:PresNs.ApplicationNonVisualDrawingProperties")) $gf.Append($nvGfp) $totalWidth = ($ColWidthsIn | Measure-Object -Sum).Sum $totalHeight = 0.0 foreach ($r in $Rows) { $totalHeight += ([double]$r.Height.Value / 914400.0) } $xfrm = New-ScoutEl "$Script:PresNs.Transform" $off = New-ScoutEl "$Script:DrawNs.Offset"; $off.X = (ScoutEmu $X); $off.Y = (ScoutEmu $Y) $ext = New-ScoutEl "$Script:DrawNs.Extents"; $ext.Cx = (ScoutEmu $totalWidth); $ext.Cy = (ScoutEmu $totalHeight) $xfrm.Append($off); $xfrm.Append($ext) $gf.Append($xfrm) $graphic = New-ScoutEl "$Script:DrawNs.Graphic" $graphicData = New-ScoutEl "$Script:DrawNs.GraphicData" $graphicData.Uri = 'http://schemas.openxmlformats.org/drawingml/2006/table' $table = New-ScoutEl "$Script:DrawNs.Table" $tblPr = New-ScoutEl "$Script:DrawNs.TableProperties" $tblPr.FirstRow = $true $tblPr.BandRow = $true $table.Append($tblPr) $grid = New-ScoutEl "$Script:DrawNs.TableGrid" foreach ($w in $ColWidthsIn) { $gc = New-ScoutEl "$Script:DrawNs.GridColumn" $gc.Width = (ScoutEmu $w) $grid.Append($gc) } $table.Append($grid) foreach ($r in $Rows) { $table.Append($r) } $graphicData.Append($table) $graphic.Append($graphicData) $gf.Append($graphic) return , $gf } #endregion #region Data helpers (safe property access, score/severity bands, pagination) function Get-ScoutProp { param($Obj, [Parameter(Mandatory)][string]$Name, $Default = $null) if ($null -eq $Obj) { return $Default } $p = $Obj.PSObject.Properties[$Name] if ($p) { return $p.Value } else { return $Default } } function Get-ScoutScoreColor { param($Score) if ($null -eq $Score) { return '595959' } if ($Score -ge 80) { return '2E7D32' } if ($Score -ge 50) { return 'B8860B' } return 'B00020' } $Script:ScoutSeverityRank = @{ high = 0; medium = 1; low = 2 } function Get-ScoutSeverityRank { # AB#5089: null/missing/unrecognized severity sorts LAST, never throws. param($Severity) if ($Severity) { $key = $Severity.ToString().Trim().ToLowerInvariant() if ($Script:ScoutSeverityRank.ContainsKey($key)) { return $Script:ScoutSeverityRank[$key] } } return 99 } function Get-ScoutPptxEvidenceSummary { <# .SYNOPSIS The supporting number for one finding, as a short table cell. .DESCRIPTION AB#6862/AB#6892, clause W-14. Mirrors Get-ScoutDocxEvidenceSummary in Export-Word.ps1. The hard case is a finding with NO evidence, which Phase 0 found on 42 of 57 failing controls. Those are not renderer failures: an `exists` rule fails precisely BECAUSE its query matched nothing, so there is no resource to name and there never will be. The row still owes the reader the scope, so it says "None found" -- an empty cell reads as a rule that never ran. #> [OutputType([string])] param($Gap) $count = Get-ScoutProp $Gap 'EvidenceCount' $denom = Get-ScoutProp $Gap 'Denominator' if ($null -eq $count) { return '' } $n = 0 try { $n = [int]$count } catch { return '' } if ($null -ne $denom) { $m = 0 try { $m = [int]$denom } catch { $m = 0 } if ($m -gt 0) { return "$n of $m" } } if ($n -eq 0) { return 'None found' } if ($n -eq 1) { return '1 resource' } return "$n resources" } function Get-ScoutSeverityLabel { # AB#5089 companion: always renders a label, never crashes on null/blank. param($Severity) if ($Severity -and "$Severity".Trim()) { return "$Severity".ToUpperInvariant() } return 'UNKNOWN' } function Get-ScoutSeverityColor { param($Severity) if (-not $Severity) { return '595959' } switch ($Severity.ToString().Trim().ToLowerInvariant()) { 'high' { return 'B00020' } 'medium' { return 'B8860B' } 'low' { return '2E75B6' } default { return '595959' } } } function Split-ScoutChunks { param([array]$Items, [int]$Size) $chunks = [System.Collections.Generic.List[object]]::new() if (-not $Items -or $Items.Count -eq 0) { return $chunks } for ($i = 0; $i -lt $Items.Count; $i += $Size) { $endIdx = [Math]::Min($i + $Size, $Items.Count) - 1 $chunks.Add(@($Items[$i..$endIdx])) } return $chunks } #endregion #region Palette $Script:Navy = '1F4E78' $Script:Steel = '2E75B6' $Script:Green = '2E7D32' $Script:Gold = 'B8860B' $Script:Red = 'B00020' $Script:Ink = '1A1A1A' $Script:Paper = 'FFFFFF' $Script:Mist = 'F6F9FD' $Script:Line = 'E2E2E2' $Script:Gray = '595959' # AB#6886, clause P-05. A roll-up deck is bounded, and the bound is a design decision rather # than an accident of how many areas an estate happens to have: 11 slides is the reference # deliverable's readout, and a deck that grows past this stops being a readout. $Script:ScoutDeckMaxSlides = 15 $Script:SlideWIn = 13.333 $Script:SlideHIn = 7.5 #endregion #region Deck shell — theme, slide master, two slide layouts (title-as-code template) function New-ScoutDeckShell { param([Parameter(Mandatory)][string]$OutFile) if (Test-Path $OutFile) { Remove-Item $OutFile -Force } $doc = [DocumentFormat.OpenXml.Packaging.PresentationDocument]::Create($OutFile, [DocumentFormat.OpenXml.PresentationDocumentType]::Presentation) $presPart = $doc.AddPresentationPart() $presPart.Presentation = New-ScoutEl "$Script:PresNs.Presentation" function New-ScoutEmptyShapeTree { $tree = New-ScoutEl "$Script:PresNs.ShapeTree" $nvGrp = New-ScoutEl "$Script:PresNs.NonVisualGroupShapeProperties" $cnv = New-ScoutEl "$Script:PresNs.NonVisualDrawingProperties" $cnv.Id = [uint32]1 $cnv.Name = '' $nvGrp.Append($cnv) $nvGrp.Append((New-ScoutEl "$Script:PresNs.NonVisualGroupShapeDrawingProperties")) $nvGrp.Append((New-ScoutEl "$Script:PresNs.ApplicationNonVisualDrawingProperties")) $tree.Append($nvGrp) $tree.Append((New-ScoutEl "$Script:PresNs.GroupShapeProperties")) return , $tree } # ---- Slide master ---- $masterPart = New-ScoutPart $presPart ([DocumentFormat.OpenXml.Packaging.SlideMasterPart]) 'rIdMaster1' $master = New-ScoutEl "$Script:PresNs.SlideMaster" $mcsd = New-ScoutEl "$Script:PresNs.CommonSlideData" $mcsd.Append((New-ScoutEmptyShapeTree)) $master.Append($mcsd) $cm = New-ScoutEl "$Script:PresNs.ColorMap" $cm.Background1 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Light1 $cm.Text1 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Dark1 $cm.Background2 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Light2 $cm.Text2 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Dark2 $cm.Accent1 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent1 $cm.Accent2 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent2 $cm.Accent3 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent3 $cm.Accent4 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent4 $cm.Accent5 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent5 $cm.Accent6 = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Accent6 $cm.Hyperlink = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::Hyperlink $cm.FollowedHyperlink = [DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues]::FollowedHyperlink $master.Append($cm) $sliList = New-ScoutEl "$Script:PresNs.SlideLayoutIdList" $sliTitle = New-ScoutEl "$Script:PresNs.SlideLayoutId"; $sliTitle.Id = [uint32]2147483649; $sliTitle.RelationshipId = 'rIdLayoutTitle' $sliContent = New-ScoutEl "$Script:PresNs.SlideLayoutId"; $sliContent.Id = [uint32]2147483650; $sliContent.RelationshipId = 'rIdLayoutContent' $sliList.Append($sliTitle); $sliList.Append($sliContent) $master.Append($sliList) $ts = New-ScoutEl "$Script:PresNs.TextStyles" $ts.Append((New-ScoutEl "$Script:PresNs.TitleStyle")) $ts.Append((New-ScoutEl "$Script:PresNs.BodyStyle")) $ts.Append((New-ScoutEl "$Script:PresNs.OtherStyle")) $master.Append($ts) $masterPart.SlideMaster = $master # ---- Slide layouts (title + content) ---- $layoutTitlePart = New-ScoutPart $masterPart ([DocumentFormat.OpenXml.Packaging.SlideLayoutPart]) 'rIdLayoutTitle' $layoutTitle = New-ScoutEl "$Script:PresNs.SlideLayout" $layoutTitle.Type = [DocumentFormat.OpenXml.Presentation.SlideLayoutValues]::Title $ltCsd = New-ScoutEl "$Script:PresNs.CommonSlideData" $ltCsd.Name = 'Azure Scout Title' $ltCsd.Append((New-ScoutEmptyShapeTree)) $layoutTitle.Append($ltCsd) $ltCmo = New-ScoutEl "$Script:PresNs.ColorMapOverride"; $ltCmo.Append((New-ScoutEl "$Script:DrawNs.MasterColorMapping")) $layoutTitle.Append($ltCmo) $layoutTitlePart.SlideLayout = $layoutTitle $layoutContentPart = New-ScoutPart $masterPart ([DocumentFormat.OpenXml.Packaging.SlideLayoutPart]) 'rIdLayoutContent' $layoutContent = New-ScoutEl "$Script:PresNs.SlideLayout" $layoutContent.Type = [DocumentFormat.OpenXml.Presentation.SlideLayoutValues]::Text $lcCsd = New-ScoutEl "$Script:PresNs.CommonSlideData" $lcCsd.Name = 'Azure Scout Content' $lcCsd.Append((New-ScoutEmptyShapeTree)) $layoutContent.Append($lcCsd) $lcCmo = New-ScoutEl "$Script:PresNs.ColorMapOverride"; $lcCmo.Append((New-ScoutEl "$Script:DrawNs.MasterColorMapping")) $layoutContent.Append($lcCmo) $layoutContentPart.SlideLayout = $layoutContent # ---- Theme (navy/steel/gold corporate palette, matches report.html.template) ---- $themePart = New-ScoutPart $masterPart ([DocumentFormat.OpenXml.Packaging.ThemePart]) 'rIdTheme1' $theme = New-ScoutEl "$Script:DrawNs.Theme" $theme.Name = 'Azure Scout' $colorScheme = New-ScoutEl "$Script:DrawNs.ColorScheme" $colorScheme.Name = 'Azure Scout' $dk1 = New-ScoutEl "$Script:DrawNs.Dark1Color" $sysDk = New-ScoutEl "$Script:DrawNs.SystemColor"; $sysDk.Val = [DocumentFormat.OpenXml.Drawing.SystemColorValues]::WindowText; $sysDk.LastColor = $Script:Ink $dk1.Append($sysDk) $lt1 = New-ScoutEl "$Script:DrawNs.Light1Color" $sysLt = New-ScoutEl "$Script:DrawNs.SystemColor"; $sysLt.Val = [DocumentFormat.OpenXml.Drawing.SystemColorValues]::Window; $sysLt.LastColor = $Script:Paper $lt1.Append($sysLt) $dk2 = New-ScoutEl "$Script:DrawNs.Dark2Color"; $dk2.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")) $dk2.RgbColorModelHex.Val = $Script:Navy $lt2 = New-ScoutEl "$Script:DrawNs.Light2Color"; $lt2.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")) $lt2.RgbColorModelHex.Val = $Script:Mist $a1 = New-ScoutEl "$Script:DrawNs.Accent1Color"; $a1.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a1.RgbColorModelHex.Val = $Script:Navy $a2 = New-ScoutEl "$Script:DrawNs.Accent2Color"; $a2.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a2.RgbColorModelHex.Val = $Script:Steel $a3 = New-ScoutEl "$Script:DrawNs.Accent3Color"; $a3.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a3.RgbColorModelHex.Val = $Script:Green $a4 = New-ScoutEl "$Script:DrawNs.Accent4Color"; $a4.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a4.RgbColorModelHex.Val = $Script:Gold $a5 = New-ScoutEl "$Script:DrawNs.Accent5Color"; $a5.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a5.RgbColorModelHex.Val = $Script:Red $a6 = New-ScoutEl "$Script:DrawNs.Accent6Color"; $a6.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $a6.RgbColorModelHex.Val = $Script:Gray $hl = New-ScoutEl "$Script:DrawNs.Hyperlink"; $hl.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $hl.RgbColorModelHex.Val = $Script:Steel $fhl = New-ScoutEl "$Script:DrawNs.FollowedHyperlinkColor"; $fhl.Append((New-ScoutEl "$Script:DrawNs.RgbColorModelHex")); $fhl.RgbColorModelHex.Val = $Script:Navy $colorScheme.Append($dk1, $lt1, $dk2, $lt2, $a1, $a2, $a3, $a4, $a5, $a6, $hl, $fhl) $fontScheme = New-ScoutEl "$Script:DrawNs.FontScheme" $fontScheme.Name = 'Azure Scout' $majorFont = New-ScoutEl "$Script:DrawNs.MajorFont" $majorLatin = New-ScoutEl "$Script:DrawNs.LatinFont"; $majorLatin.Typeface = 'Segoe UI Semibold' $majorFont.Append($majorLatin); $majorFont.Append((New-ScoutEl "$Script:DrawNs.EastAsianFont")); $majorFont.Append((New-ScoutEl "$Script:DrawNs.ComplexScriptFont")) $minorFont = New-ScoutEl "$Script:DrawNs.MinorFont" $minorLatin = New-ScoutEl "$Script:DrawNs.LatinFont"; $minorLatin.Typeface = 'Segoe UI' $minorFont.Append($minorLatin); $minorFont.Append((New-ScoutEl "$Script:DrawNs.EastAsianFont")); $minorFont.Append((New-ScoutEl "$Script:DrawNs.ComplexScriptFont")) $fontScheme.Append($majorFont, $minorFont) $fmtScheme = New-ScoutEl "$Script:DrawNs.FormatScheme" $fmtScheme.Name = 'Azure Scout' $fillList = New-ScoutEl "$Script:DrawNs.FillStyleList" 1..3 | ForEach-Object { $s = New-ScoutEl "$Script:DrawNs.SolidFill"; $s.Append((New-ScoutPhColor)); $fillList.Append($s) } $lineList = New-ScoutEl "$Script:DrawNs.LineStyleList" 1..3 | ForEach-Object { $ol = New-ScoutEl "$Script:DrawNs.Outline"; $ol.Width = 9525 $olFill = New-ScoutEl "$Script:DrawNs.SolidFill"; $olFill.Append((New-ScoutPhColor)) $ol.Append($olFill) $lineList.Append($ol) } $effList = New-ScoutEl "$Script:DrawNs.EffectStyleList" 1..3 | ForEach-Object { $es = New-ScoutEl "$Script:DrawNs.EffectStyle"; $es.Append((New-ScoutEl "$Script:DrawNs.EffectList")); $effList.Append($es) } $bgList = New-ScoutEl "$Script:DrawNs.BackgroundFillStyleList" 1..3 | ForEach-Object { $s = New-ScoutEl "$Script:DrawNs.SolidFill"; $s.Append((New-ScoutPhColor)); $bgList.Append($s) } $fmtScheme.Append($fillList, $lineList, $effList, $bgList) $themeElements = New-ScoutEl "$Script:DrawNs.ThemeElements" $themeElements.Append($colorScheme, $fontScheme, $fmtScheme) $theme.Append($themeElements) $theme.Append((New-ScoutEl "$Script:DrawNs.ObjectDefaults")) $theme.Append((New-ScoutEl "$Script:DrawNs.ExtraColorSchemeList")) $themePart.Theme = $theme return [pscustomobject]@{ Doc = $doc PresPart = $presPart MasterPart = $masterPart LayoutTitlePart = $layoutTitlePart LayoutContentPart = $layoutContentPart NextSlideRelIdNum = 1 NextSlideIdNum = 256 } } #endregion #region Slide chrome + slide assembly function Add-ScoutSlideChrome { # Appends the shared navy-bar / title / gold-underline / footer shapes # that make every content slide read as one consistent template. param($Tree, [string]$Title, [int]$PageNum, [int]$TotalPages) $bar = New-ScoutShape -Name 'TopBar' -X 0 -Y 0 -Cx $Script:SlideWIn -Cy 0.09 -FillHex $Script:Navy $Tree.Append($bar) $titleParas = New-ScoutList $titleRuns = New-ScoutList $titleRuns.Add((New-ScoutRun -Text $Title -SizePt 26 -Hex $Script:Navy -Bold $true -Font 'Segoe UI Semibold')) $titleParas.Add((New-ScoutPara -Runs $titleRuns)) $titleShape = New-ScoutShape -Name 'SlideTitle' -X 0.55 -Y 0.26 -Cx 12.2 -Cy 0.62 -Paragraphs $titleParas $Tree.Append($titleShape) $underline = New-ScoutShape -Name 'TitleUnderline' -X 0.55 -Y 0.92 -Cx 1.1 -Cy 0.045 -FillHex $Script:Gold $Tree.Append($underline) $divider = New-ScoutShape -Name 'FooterDivider' -X 0.55 -Y 7.02 -Cx 12.23 -Cy 0.012 -FillHex $Script:Line $Tree.Append($divider) $wmParas = New-ScoutList $wmRuns = New-ScoutList $wmRuns.Add((New-ScoutRun -Text 'AZURE SCOUT' -SizePt 9 -Hex $Script:Gray -Bold $true)) $wmParas.Add((New-ScoutPara -Runs $wmRuns)) $wordmark = New-ScoutShape -Name 'FooterWordmark' -X 0.55 -Y 7.09 -Cx 4 -Cy 0.3 -Paragraphs $wmParas $Tree.Append($wordmark) if ($PageNum -gt 0) { $pgParas = New-ScoutList $pgRuns = New-ScoutList $pgRuns.Add((New-ScoutRun -Text "$PageNum / $TotalPages" -SizePt 9 -Hex $Script:Gray)) $pgParas.Add((New-ScoutPara -Runs $pgRuns -Align 'r')) $pgShape = New-ScoutShape -Name 'FooterPage' -X 10.78 -Y 7.09 -Cx 2 -Cy 0.3 -Paragraphs $pgParas $Tree.Append($pgShape) } } function New-ScoutEmptyShapeTreeStandalone { # Same structure as the shell's private helper, exposed here for slide bodies. $tree = New-ScoutEl "$Script:PresNs.ShapeTree" $nvGrp = New-ScoutEl "$Script:PresNs.NonVisualGroupShapeProperties" $cnv = New-ScoutEl "$Script:PresNs.NonVisualDrawingProperties" $cnv.Id = [uint32]1 $cnv.Name = '' $nvGrp.Append($cnv) $nvGrp.Append((New-ScoutEl "$Script:PresNs.NonVisualGroupShapeDrawingProperties")) $nvGrp.Append((New-ScoutEl "$Script:PresNs.ApplicationNonVisualDrawingProperties")) $tree.Append($nvGrp) $tree.Append((New-ScoutEl "$Script:PresNs.GroupShapeProperties")) return , $tree } function New-ScoutSlideElement { param($Tree) $slide = New-ScoutEl "$Script:PresNs.Slide" $scsd = New-ScoutEl "$Script:PresNs.CommonSlideData" $scsd.Append($Tree) $slide.Append($scsd) $cmo = New-ScoutEl "$Script:PresNs.ColorMapOverride" $cmo.Append((New-ScoutEl "$Script:DrawNs.MasterColorMapping")) $slide.Append($cmo) return , $slide } function Add-ScoutSlideToDeck { # Creates the SlidePart for a fully-built Slide element, wires it to the # given layout part, and registers it in the presentation's SlideIdList. param($Shell, $SlideElement, $LayoutPart) $relId = "rIdSlide$($Shell.NextSlideRelIdNum)" $slidePart = New-ScoutPart $Shell.PresPart ([DocumentFormat.OpenXml.Packaging.SlidePart]) $relId $slidePart.Slide = $SlideElement $null = $slidePart.AddPart($LayoutPart) if (-not $Shell.PSObject.Properties['SlideIdList']) { $Shell | Add-Member -NotePropertyName SlideIdList -NotePropertyValue (New-ScoutEl "$Script:PresNs.SlideIdList") } $sid = New-ScoutEl "$Script:PresNs.SlideId" $sid.Id = [uint32]$Shell.NextSlideIdNum $sid.RelationshipId = $Shell.PresPart.GetIdOfPart($slidePart) $Shell.SlideIdList.Append($sid) $Shell.NextSlideRelIdNum++ $Shell.NextSlideIdNum++ } #endregion #region Slide content builders function New-ScoutTitleSlide { param([Parameter(Mandatory)]$Shell, [string]$Title, [string]$Subtitle, [string]$MetaLine) $tree = New-ScoutEmptyShapeTreeStandalone $bg = New-ScoutShape -Name 'TitleBackground' -X 0 -Y 0 -Cx $Script:SlideWIn -Cy $Script:SlideHIn -FillHex $Script:Navy $tree.Append($bg) $wmParas = New-ScoutList; $wmRuns = New-ScoutList $wmRuns.Add((New-ScoutRun -Text 'AZURE SCOUT' -SizePt 14 -Hex $Script:Paper -Bold $true)) $wmParas.Add((New-ScoutPara -Runs $wmRuns)) $tree.Append((New-ScoutShape -Name 'Wordmark' -X 0.6 -Y 0.45 -Cx 5 -Cy 0.4 -Paragraphs $wmParas)) $accent = New-ScoutShape -Name 'TitleAccent' -X 5.67 -Y 2.45 -Cx 2 -Cy 0.05 -FillHex $Script:Gold $tree.Append($accent) $titleParas = New-ScoutList; $titleRuns = New-ScoutList $titleRuns.Add((New-ScoutRun -Text $Title -SizePt 40 -Hex $Script:Paper -Bold $true -Font 'Segoe UI Semibold')) $titleParas.Add((New-ScoutPara -Runs $titleRuns -Align 'ctr')) $tree.Append((New-ScoutShape -Name 'Title' -X 1 -Y 2.65 -Cx 11.33 -Cy 1.1 -Paragraphs $titleParas -Anchor 'ctr')) if ($Subtitle) { $subParas = New-ScoutList; $subRuns = New-ScoutList $subRuns.Add((New-ScoutRun -Text $Subtitle -SizePt 18 -Hex 'D9E4EF')) $subParas.Add((New-ScoutPara -Runs $subRuns -Align 'ctr')) $tree.Append((New-ScoutShape -Name 'Subtitle' -X 1 -Y 3.75 -Cx 11.33 -Cy 0.55 -Paragraphs $subParas -Anchor 'ctr')) } if ($MetaLine) { $metaParas = New-ScoutList; $metaRuns = New-ScoutList $metaRuns.Add((New-ScoutRun -Text $MetaLine -SizePt 13 -Hex 'AFC3D9')) $metaParas.Add((New-ScoutPara -Runs $metaRuns -Align 'ctr')) $tree.Append((New-ScoutShape -Name 'MetaLine' -X 1 -Y 4.35 -Cx 11.33 -Cy 0.5 -Paragraphs $metaParas -Anchor 'ctr')) } $footParas = New-ScoutList; $footRuns = New-ScoutList $footRuns.Add((New-ScoutRun -Text 'Confidential — prepared by Azure Scout' -SizePt 10 -Hex 'AFC3D9')) $footParas.Add((New-ScoutPara -Runs $footRuns -Align 'ctr')) $tree.Append((New-ScoutShape -Name 'TitleFooter' -X 1 -Y 6.85 -Cx 11.33 -Cy 0.4 -Paragraphs $footParas -Anchor 'ctr')) $slide = New-ScoutSlideElement -Tree $tree Add-ScoutSlideToDeck -Shell $Shell -SlideElement $slide -LayoutPart $Shell.LayoutTitlePart } function New-ScoutContentSlide { # $BodyShapeBuilder: scriptblock that appends body shapes onto $tree (receives $tree as arg). param([Parameter(Mandatory)]$Shell, [string]$Title, [int]$PageNum, [int]$TotalPages, [scriptblock]$BodyShapeBuilder) $tree = New-ScoutEmptyShapeTreeStandalone Add-ScoutSlideChrome -Tree $tree -Title $Title -PageNum $PageNum -TotalPages $TotalPages & $BodyShapeBuilder $tree $slide = New-ScoutSlideElement -Tree $tree Add-ScoutSlideToDeck -Shell $Shell -SlideElement $slide -LayoutPart $Shell.LayoutContentPart } function Add-ScoutSlidePicture { <# .SYNOPSIS Place a rasterised figure on a slide as an embedded picture. .DESCRIPTION AB#6883, clauses W-12/D-03 applied to the deck. The picture is EMBEDDED as a part of the package, never linked: a deck emailed to a client has to carry its own images, and a linked one is a broken one the moment the file leaves the machine that built it. The image part has to be added to the SLIDE part, not the presentation part, and the relationship id is resolved from the slide -- which is why this runs after the slide part exists rather than while the shape tree is being built. EMU are the DrawingML unit: 914,400 per inch, 9,525 per pixel at 96 DPI. The figure is scaled to fit the box while preserving aspect, because a picture stretched to a box of a different ratio is worse than a smaller one. #> param( [Parameter(Mandatory)]$SlidePart, [Parameter(Mandatory)]$Tree, [Parameter(Mandatory)]$Figure, [Parameter(Mandatory)][int]$Id, [double]$X, [double]$Y, [double]$BoxWIn, [double]$BoxHIn ) # AddNewPart<ImagePart>(contentType): the AddImagePart convenience overload was removed in # DocumentFormat.OpenXml 3.x, which is the version this renderer pins. $imagePart = $SlidePart.AddNewPart[DocumentFormat.OpenXml.Packaging.ImagePart]('image/png') $ms = [System.IO.MemoryStream]::new([byte[]]$Figure.Bytes) try { $imagePart.FeedData($ms) } finally { $ms.Dispose() } $relId = $SlidePart.GetIdOfPart($imagePart) # Fit inside the box, preserving aspect. $scale = [Math]::Min($BoxWIn / ($Figure.Width / 96.0), $BoxHIn / ($Figure.Height / 96.0)) $wIn = ($Figure.Width / 96.0) * $scale $hIn = ($Figure.Height / 96.0) * $scale $offX = $X + (($BoxWIn - $wIn) / 2) $cx = [int64]($wIn * 914400) $cy = [int64]($hIn * 914400) $ox = [int64]($offX * 914400) $oy = [int64]($Y * 914400) $safeName = ConvertTo-ScoutPptxXmlText $Figure.Name $pic = New-ScoutEl "$Script:PresNs.Picture" $pic.InnerXml = @" <p:nvPicPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"> <p:cNvPr id="$Id" name="$safeName"/> <p:cNvPicPr><a:picLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></p:cNvPicPr> <p:nvPr/> </p:nvPicPr> <p:blipFill xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"> <a:blip xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="$relId"/> <a:stretch xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:fillRect/></a:stretch> </p:blipFill> <p:spPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"> <a:xfrm xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"> <a:off x="$ox" y="$oy"/><a:ext cx="$cx" cy="$cy"/> </a:xfrm> <a:prstGeom xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" prst="rect"><a:avLst/></a:prstGeom> </p:spPr> "@ $Tree.Append($pic) } function ConvertTo-ScoutPptxXmlText { # Figure names are generated, but escaping is cheap and a malformed name would produce a # package PowerPoint refuses to open rather than a visibly wrong one. param([AllowEmptyString()][AllowNull()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return 'figure' } return ($Text -replace '&', '&' -replace '<', '<' -replace '>', '>' -replace '"', '"') } function New-ScoutFigureSlides { <# .SYNOPSIS One slide per rasterised figure, with the caption beneath it. .DESCRIPTION AB#6883. One figure per slide rather than a grid: the deck's rule is one idea per slide, and three charts crammed onto one is three ideas nobody reads. Non-fatal by design. A deck that loses its figures is still a deck; a run that dies while adding a picture has cost the operator every other format too. #> param($Shell, $Figures, [ref]$PageCounter, [int]$TotalPages) $figs = @($Figures) if ($figs.Count -eq 0) { return } $picId = 1000 foreach ($fig in $figs) { $picId++ $captured = $fig $capturedId = $picId try { $tree = New-ScoutEmptyShapeTreeStandalone Add-ScoutSlideChrome -Tree $tree -Title $captured.Caption -PageNum $PageCounter.Value -TotalPages $TotalPages $slideEl = New-ScoutSlideElement -Tree $tree # The slide part has to exist before an image part can hang off it, so the deck # registration happens first and the picture is appended to the live tree after. Add-ScoutSlideToDeck -Shell $Shell -SlideElement $slideEl -LayoutPart $Shell.LayoutContentPart $slidePart = $Shell.PresPart.SlideParts | Select-Object -Last 1 Add-ScoutSlidePicture -SlidePart $slidePart -Tree $tree -Figure $captured -Id $capturedId ` -X 0.75 -Y 1.35 -BoxWIn 11.8 -BoxHIn 5.4 $PageCounter.Value++ } catch { Write-Warning "Export-Pptx: figure '$($captured.Name)' could not be placed on a slide ($($_.Exception.Message)) -- omitted." } } } function Add-ScoutBulletList { param($Tree, [double]$X, [double]$Y, [double]$Cx, [string[]]$Lines, [double]$SizePt = 15, [double]$LineGapIn = 0.5) $paras = New-ScoutList foreach ($line in $Lines) { $runs = New-ScoutList $runs.Add((New-ScoutRun -Text "• $line" -SizePt $SizePt -Hex $Script:Ink)) $paras.Add((New-ScoutPara -Runs $runs -SpaceBeforePt 6)) } $height = [math]::Max($LineGapIn * $Lines.Count, 0.4) $Tree.Append((New-ScoutShape -Name 'BulletList' -X $X -Y $Y -Cx $Cx -Cy $height -Paragraphs $paras)) } function Add-ScoutScoreCard { param($Tree, [double]$X, [double]$Y, [double]$Cx, [double]$Cy, [string]$Label, $Score) $color = Get-ScoutScoreColor $Score $scoreText = if ($null -eq $Score) { '—' } else { "$Score" } $paras = New-ScoutList $bigRuns = New-ScoutList $bigRuns.Add((New-ScoutRun -Text $scoreText -SizePt 44 -Hex $Script:Paper -Bold $true -Font 'Segoe UI Semibold')) $paras.Add((New-ScoutPara -Runs $bigRuns -Align 'ctr')) $lblRuns = New-ScoutList $lblRuns.Add((New-ScoutRun -Text $Label -SizePt 13 -Hex $Script:Paper)) $paras.Add((New-ScoutPara -Runs $lblRuns -Align 'ctr')) $Tree.Append((New-ScoutShape -Name "Card_$Label" -X $X -Y $Y -Cx $Cx -Cy $Cy -FillHex $color -Anchor 'ctr' -RadiusPct 6 -Paragraphs $paras)) } function New-ScoutExecSummarySlide { param($Shell, $Frameworks, $Areas, $Gaps, $Manual, $Errors, [int]$PageNum, [int]$TotalPages) New-ScoutContentSlide -Shell $Shell -Title 'Executive Summary' -PageNum $PageNum -TotalPages $TotalPages -BodyShapeBuilder { param($tree) $cardW = 2.7 $gap = 0.25 $startX = 0.55 $i = 0 if (@($Frameworks).Count -eq 0) { Add-ScoutScoreCard -Tree $tree -X $startX -Y 1.25 -Cx $cardW -Cy 1.7 -Label 'No framework scored' -Score $null } else { foreach ($fw in @($Frameworks)) { $x = $startX + ($i * ($cardW + $gap)) $label = "$(Get-ScoutProp $fw 'Framework') Alignment Score" Add-ScoutScoreCard -Tree $tree -X $x -Y 1.25 -Cx $cardW -Cy 1.7 -Label $label -Score (Get-ScoutProp $fw 'Score') $i++ } } $areaArr = @($Areas) $passSum = ($areaArr | ForEach-Object { Get-ScoutProp $_ 'Pass' 0 } | Measure-Object -Sum).Sum $partialSum = ($areaArr | ForEach-Object { Get-ScoutProp $_ 'Partial' 0 } | Measure-Object -Sum).Sum $failSum = ($areaArr | ForEach-Object { Get-ScoutProp $_ 'Fail' 0 } | Measure-Object -Sum).Sum $manualCount = @($Manual).Count $errorCount = @($Errors).Count $highGaps = (@($Gaps) | Where-Object { (Get-ScoutSeverityLabel (Get-ScoutProp $_ 'Severity')) -eq 'HIGH' } | Measure-Object).Count $lines = @( "Areas assessed: $($areaArr.Count)" "Rules evaluated — Pass: $passSum, Partial: $partialSum, Fail: $failSum" "Critical (High severity) gaps: $highGaps" "Manual review items pending: $manualCount" "Unknown/Error findings (check collector permissions): $errorCount" ) Add-ScoutBulletList -Tree $tree -X 0.55 -Y 3.5 -Cx 12.2 -Lines $lines -SizePt 15 -LineGapIn 0.5 } } function New-ScoutAreaTableSlides { # $MaxPages is the slide budget clause P-05 leaves this section after the fixed slides. An # estate with 60 areas would otherwise produce a six-page table and blow the cap; it is # truncated with a stated remainder rather than silently, because a table that stops without # saying so reads as the whole list. param($Shell, $Areas, [ref]$PageCounter, [int]$TotalPages, [int]$MaxPages = 0) # @(...) wraps the WHOLE pipeline, not just $Areas -- a Sort-Object over zero # input collapses the bare assignment to $null, and $null.Count throws # PropertyNotFoundException under Set-StrictMode -Version Latest. Same # load-bearing pattern Get-Score.ps1 documents for its own Pass/Fail counters. $rows = @(@($Areas) | Sort-Object Framework, Area) if ($rows.Count -eq 0) { return } $chunks = @(Split-ScoutChunks -Items $rows -Size 10) $dropped = 0 if ($MaxPages -gt 0 -and $chunks.Count -gt $MaxPages) { $shown = $MaxPages * 10 $dropped = $rows.Count - $shown $chunks = @($chunks | Select-Object -First $MaxPages) } $pageOfPages = $chunks.Count $chunkIdx = 0 foreach ($chunk in $chunks) { $chunkIdx++ $title = if ($pageOfPages -gt 1) { "Area Score Breakdown ($chunkIdx/$pageOfPages)" } else { 'Area Score Breakdown' } if ($dropped -gt 0 -and $chunkIdx -eq $pageOfPages) { $title = "$title — $dropped more in the workbook" } $capturedChunk = $chunk New-ScoutContentSlide -Shell $Shell -Title $title -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) $colW = @(1.6, 3.5, 1.0, 0.9, 0.9, 0.9, 1.0) $rowsList = New-ScoutList $headerCells = New-ScoutList foreach ($h in 'Framework', 'Area', 'Score', 'Pass', 'Partial', 'Fail', 'Manual') { $headerCells.Add((New-ScoutTableCell -Text $h -SizePt 12 -Hex $Script:Paper -Bold $true -FillHex $Script:Navy)) } $rowsList.Add((New-ScoutTableRow -HeightIn 0.38 -Cells $headerCells)) $r = 0 foreach ($area in $capturedChunk) { $r++ $bg = if ($r % 2 -eq 0) { $Script:Mist } else { $Script:Paper } $score = Get-ScoutProp $area 'Score' $scoreColor = Get-ScoutScoreColor $score $scoreText = if ($null -eq $score) { '—' } else { "$score" } $cells = New-ScoutList $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Framework')" -Hex $Script:Ink -FillHex $bg)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Area')" -Hex $Script:Ink -FillHex $bg -Align 'l')) $cells.Add((New-ScoutTableCell -Text $scoreText -Bold $true -Hex $scoreColor -FillHex $bg)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Pass' 0)" -Hex $Script:Ink -FillHex $bg)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Partial' 0)" -Hex $Script:Ink -FillHex $bg)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Fail' 0)" -Hex $Script:Ink -FillHex $bg)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $area 'Manual' 0)" -Hex $Script:Ink -FillHex $bg)) $rowsList.Add((New-ScoutTableRow -HeightIn 0.32 -Cells $cells)) } $tree.Append((New-ScoutTable -Name 'AreaTable' -X 0.55 -Y 1.25 -ColWidthsIn $colW -Rows $rowsList)) } $PageCounter.Value++ } } function New-ScoutGapsSlides { param($Shell, $Gaps, [ref]$PageCounter, [int]$TotalPages, [int]$MaxGaps = 15) # AB#5089: defensive re-sort at render time too — null/unrecognized severity # sorts LAST — even if the caller passes gaps that were never run through # Get-Score's own sort. Both @(...) wraps are load-bearing: a Sort-Object/ # Select-Object over zero input collapses the bare assignment to $null, and # $null.Count throws PropertyNotFoundException under Set-StrictMode -Version Latest. $sorted = @(@($Gaps) | Sort-Object @{ Expression = { Get-ScoutSeverityRank (Get-ScoutProp $_ 'Severity') } }, Area) $top = @($sorted | Select-Object -First $MaxGaps) if ($top.Count -eq 0) { New-ScoutContentSlide -Shell $Shell -Title 'Prioritized Gaps' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) Add-ScoutBulletList -Tree $tree -X 0.55 -Y 1.6 -Cx 12.2 -Lines @('No prioritized gaps — all assessed rules are passing.') -SizePt 16 } $PageCounter.Value++ return } $chunks = Split-ScoutChunks -Items $top -Size 10 $pageOfPages = $chunks.Count $chunkIdx = 0 foreach ($chunk in $chunks) { $chunkIdx++ $title = if ($pageOfPages -gt 1) { "Prioritized Gaps (Top $($top.Count)) — $chunkIdx/$pageOfPages" } else { "Prioritized Gaps (Top $($top.Count))" } $capturedChunk = $chunk New-ScoutContentSlide -Shell $Shell -Title $title -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) # AB#6862/AB#6892, clause W-14 applied to the deck. Same defect the Word renderer had: # Severity | Area | Gap and nothing else, so every row was a verdict with no # supporting number. See Get-ScoutPptxEvidenceSummary for why the zero case matters. $colW = @(1.3, 2.4, 5.6, 1.6) $rowsList = New-ScoutList $headerCells = New-ScoutList foreach ($h in 'Severity', 'Area', 'Gap', 'Evidence') { $headerCells.Add((New-ScoutTableCell -Text $h -SizePt 12 -Hex $Script:Paper -Bold $true -FillHex $Script:Navy)) } $rowsList.Add((New-ScoutTableRow -HeightIn 0.38 -Cells $headerCells)) $r = 0 foreach ($gap in $capturedChunk) { $r++ $bg = if ($r % 2 -eq 0) { $Script:Mist } else { $Script:Paper } $sevLabel = Get-ScoutSeverityLabel (Get-ScoutProp $gap 'Severity') $sevColor = Get-ScoutSeverityColor (Get-ScoutProp $gap 'Severity') $cells = New-ScoutList $cells.Add((New-ScoutTableCell -Text $sevLabel -Bold $true -Hex $Script:Paper -FillHex $sevColor)) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $gap 'Area')" -Hex $Script:Ink -FillHex $bg -Align 'l')) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $gap 'Title')" -Hex $Script:Ink -FillHex $bg -Align 'l')) $cells.Add((New-ScoutTableCell -Text (Get-ScoutPptxEvidenceSummary $gap) -SizePt 11 -Hex $Script:Ink -FillHex $bg -Align 'l')) $rowsList.Add((New-ScoutTableRow -HeightIn 0.4 -Cells $cells)) } $tree.Append((New-ScoutTable -Name 'GapsTable' -X 0.55 -Y 1.25 -ColWidthsIn $colW -Rows $rowsList)) } $PageCounter.Value++ } } function New-ScoutManualSlide { param($Shell, $Manual, [ref]$PageCounter, [int]$TotalPages, [int]$MaxRows = 12) $items = @($Manual) if ($items.Count -eq 0) { New-ScoutContentSlide -Shell $Shell -Title 'Manual Review Worklist' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) Add-ScoutBulletList -Tree $tree -X 0.55 -Y 1.6 -Cx 12.2 -Lines @('No manual review items — full automated coverage for the selected assessment(s).') -SizePt 16 } $PageCounter.Value++ return } # @() keeps $shown a real array regardless of how many items Select-Object # -First actually returns -- Select-Object over exactly one input otherwise # collapses the bare assignment to a scalar, which is the same fragile # single-item-collapse shape the zero-item $null collapse guarded elsewhere # in this file comes from ($items.Count -eq 0 is already ruled out above). $shown = @($items | Select-Object -First $MaxRows) $truncated = $items.Count - $shown.Count New-ScoutContentSlide -Shell $Shell -Title 'Manual Review Worklist' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) $colW = @(3.5, 8.5) $rowsList = New-ScoutList $headerCells = New-ScoutList foreach ($h in 'Area', 'Item') { $headerCells.Add((New-ScoutTableCell -Text $h -SizePt 12 -Hex $Script:Paper -Bold $true -FillHex $Script:Navy)) } $rowsList.Add((New-ScoutTableRow -HeightIn 0.38 -Cells $headerCells)) $r = 0 foreach ($m in $shown) { $r++ $bg = if ($r % 2 -eq 0) { $Script:Mist } else { $Script:Paper } $cells = New-ScoutList $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $m 'Area')" -Hex $Script:Ink -FillHex $bg -Align 'l')) $cells.Add((New-ScoutTableCell -Text "$(Get-ScoutProp $m 'Title')" -Hex $Script:Ink -FillHex $bg -Align 'l')) $rowsList.Add((New-ScoutTableRow -HeightIn 0.36 -Cells $cells)) } $tree.Append((New-ScoutTable -Name 'ManualTable' -X 0.55 -Y 1.25 -ColWidthsIn $colW -Rows $rowsList)) if ($truncated -gt 0) { Add-ScoutBulletList -Tree $tree -X 0.55 -Y 6.55 -Cx 12.2 -Lines @("+$truncated more not shown — see the evidence pack (Excel tier) for the full list.") -SizePt 11 -LineGapIn 0.3 } } $PageCounter.Value++ } function New-ScoutScopeSlide { <# .SYNOPSIS What this assessment did and — more importantly — did NOT look at. .DESCRIPTION AB#6886, clause P-03. A deck that states only what it found invites the reader to assume everything else was checked and passed. Roughly 60% of a Scout run is Manual or Unknown, which is a property of the rule set (Epic AB#6454), and a deck that does not say so is overclaiming. The numbers here are computed from the run rather than written as boilerplate, so the slide cannot drift away from what was actually assessed. #> param($Shell, $Areas, $AllFindings, $Manual, $Errors, [ref]$PageCounter, [int]$TotalPages) $all = @($AllFindings) $manualCount = @($Manual).Count $errorCount = @($Errors).Count $automated = $all.Count - $manualCount - $errorCount if ($automated -lt 0) { $automated = 0 } New-ScoutContentSlide -Shell $Shell -Title 'Scope — and what was not assessed' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) $lines = @( "$automated control(s) were evaluated automatically against $(@($Areas).Count) assessed area(s)." "$manualCount control(s) require manual review and were NOT assessed. They are not passes and not failures." "$errorCount finding(s) could not be evaluated — usually a collector permission or query issue, not a compliance failure." 'Assessment depth is bounded by the rule set, not by this report. A control with no automated rule is reported as Not assessed.' 'Nothing outside the tenant and subscriptions listed on the title slide was examined.' ) Add-ScoutBulletList -Tree $tree -X 0.55 -Y 1.5 -Cx 12.2 -Lines $lines -SizePt 16 -LineGapIn 0.7 } $PageCounter.Value++ } function New-ScoutActFirstSlide { <# .SYNOPSIS The single "act on this first" slide, naming one specific item. .DESCRIPTION AB#6886, clause P-04: "exactly one 'act on this first' slide naming a specific item." Exactly one, because a deck with five priorities has none, and a named item because "address high-severity gaps" is not something anyone can be assigned on Monday. The item is chosen by the same worst-first ordering the document uses, so the deck and the report cannot disagree about what matters most. #> param($Shell, $Gaps, [ref]$PageCounter, [int]$TotalPages) $sorted = @(@($Gaps) | Sort-Object @{ Expression = { Get-ScoutSeverityRank (Get-ScoutProp $_ 'Severity') } }, Area) $top = $sorted | Select-Object -First 1 New-ScoutContentSlide -Shell $Shell -Title 'Act on this first' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) if ($null -eq $top) { $lines = @( 'No gaps were raised by this run.' 'Every automatically evaluated control in scope is aligned. The Not assessed items on the scope slide are the remaining work.' ) } else { $sev = Get-ScoutSeverityLabel (Get-ScoutProp $top 'Severity') $area = "$(Get-ScoutProp $top 'Area')" $title = "$(Get-ScoutProp $top 'Title')" $evidence = Get-ScoutPptxEvidenceSummary $top $lines = @( "$title" "Area: $area · Severity: $sev$(if ($evidence) { " · Scope: $evidence" })" "$(Get-ScoutProp $top 'Remediation')" 'This is the highest-severity gap in the run. It is one item, on purpose — a deck with five priorities has none.' ) } Add-ScoutBulletList -Tree $tree -X 0.55 -Y 1.5 -Cx 12.2 -Lines $lines -SizePt 16 -LineGapIn 0.8 } $PageCounter.Value++ } function New-ScoutNextStepsSlide { param($Shell, [ref]$PageCounter, [int]$TotalPages) New-ScoutContentSlide -Shell $Shell -Title 'Recommended Next Steps' -PageNum $PageCounter.Value -TotalPages $TotalPages -BodyShapeBuilder { param($tree) $lines = @( 'Prioritize remediation of High severity gaps identified in this report.' 'Assign owners and target dates for each Fail finding using the evidence pack (Excel tier).' 'Complete outstanding Manual review items with the platform/security team.' 'Re-run Azure Scout after remediation to track score improvement over time.' 'Revisit Unknown/Error findings — these usually indicate a collector permission or query issue, not a compliance failure.' ) Add-ScoutBulletList -Tree $tree -X 0.55 -Y 1.5 -Cx 12.2 -Lines $lines -SizePt 16 -LineGapIn 0.7 } $PageCounter.Value++ } #endregion function Export-Pptx { <# .SYNOPSIS Renders the executive PowerPoint deck for a scored Findings object. .PARAMETER Findings The scored object returned by Get-Score (GeneratedOn/Frameworks/Areas/ Gaps/Manual/Errors/Findings). .PARAMETER Collect Optional — the raw collect object (same one Export-Html/-Excel/-PowerBi already receive from Export-Report.ps1). Used only to surface scope / management-group context on the title slide when present; the deck renders fine without it (Collect carries no tenant field today — see src/collect/Invoke-Collect.ps1 — so the title slide degrades gracefully to date + branding only when Collect is absent or _meta is empty). .PARAMETER OutputPath Directory the rendered assessment_deck.pptx is written into. #> param($Findings, $Collect, [string] $OutputPath) Import-ScoutOpenXmlAssembly $outFile = Join-Path $OutputPath 'assessment_deck.pptx' New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction SilentlyContinue | Out-Null $frameworks = @(Get-ScoutProp $Findings 'Frameworks') $areas = @(Get-ScoutProp $Findings 'Areas') $gaps = @(Get-ScoutProp $Findings 'Gaps') $manual = @(Get-ScoutProp $Findings 'Manual') $errors = @(Get-ScoutProp $Findings 'Errors') $allFindings = @(Get-ScoutProp $Findings 'Findings') $generatedOn = Get-ScoutProp $Findings 'GeneratedOn' $generatedText = if ($generatedOn) { try { ([datetime]$generatedOn).ToString('yyyy-MM-dd') } catch { "$generatedOn" } } else { (Get-Date).ToString('yyyy-MM-dd') } $scope = Get-ScoutProp (Get-ScoutProp $Collect '_meta') 'scope' $mgId = Get-ScoutProp (Get-ScoutProp $Collect '_meta') 'managementGroupId' $metaParts = New-Object System.Collections.Generic.List[string] $metaParts.Add("Generated $generatedText") if ($scope) { $metaParts.Add("Scope: $scope") } if ($mgId) { $metaParts.Add("Management Group: $mgId") } $metaLine = [string]::Join(' · ', $metaParts) # Slide count plan (used for the "n / total" footer): # 1 title + 1 scope + 1 summary + 1 act-first + area pages + gap pages (>=1) + 1 manual + 1 next-steps # # AB#6886, clause P-05 caps a roll-up deck at 15 slides, and adding the scope and act-first # slides spends two of that budget. The area and gap page counts are therefore bounded below # rather than left to grow with the estate: an eleven-slide deck that says one thing per # slide beats a forty-slide deck nobody reaches the end of, and the detail those extra pages # would have carried is in the workbook and the document. # AB#6883. Figures are rendered up front so the slide budget can account for them: they are # part of the deck's fixed spine, not an optional extra tacked on after the cap was computed. # Non-fatal -- a deck without figures is still a deck. $figures = @() try { if (-not (Get-Command -Name Export-ScoutFigureSet -ErrorAction SilentlyContinue)) { . "$PSScriptRoot/../Build-ScoutFigure.ps1" } $figures = @(Export-ScoutFigureSet -Findings $Findings -OutputPath $OutputPath) } catch { Write-Warning "Export-Pptx: the figure set did not render ($($_.Exception.Message)) -- the deck ships without figures." } $fixedSlides = 6 + $figures.Count $budget = [Math]::Max(2, $Script:ScoutDeckMaxSlides - $fixedSlides) $areaPages = if (@($areas).Count -gt 0) { [Math]::Ceiling(@($areas).Count / 10.0) } else { 0 } # @() wraps the WHOLE pipeline -- Select-Object -First over zero input # collapses the bare assignment to $null, and $null.Count throws under # Set-StrictMode -Version Latest. $gapCandidates = @(@($gaps) | Select-Object -First 15) $gapPages = if ($gapCandidates.Count -gt 0) { [Math]::Ceiling($gapCandidates.Count / 10.0) } else { 1 } # Gaps keep at least one page -- a deck that drops the gap list to fit an area table has # dropped the point of the assessment. $areaPages = [Math]::Max(0, [Math]::Min($areaPages, $budget - 1)) $gapPages = [Math]::Max(1, [Math]::Min($gapPages, $budget - $areaPages)) $totalPages = $fixedSlides + $areaPages + $gapPages $shell = New-ScoutDeckShell -OutFile $outFile New-ScoutTitleSlide -Shell $shell -Title 'Azure Landing Zone Assessment' ` -Subtitle 'Executive Assessment — CAF & WAF Alignment' -MetaLine $metaLine $page = 2 New-ScoutExecSummarySlide -Shell $shell -Frameworks $frameworks -Areas $areas -Gaps $gaps -Manual $manual -Errors $errors -PageNum $page -TotalPages $totalPages $page++ $pageRef = [ref]$page # Scope before the detail (P-03) and the single priority right after it (P-04): a reader who # leaves after three slides should still know what was covered and what to do on Monday. New-ScoutScopeSlide -Shell $shell -Areas $areas -AllFindings $allFindings -Manual $manual -Errors $errors -PageCounter $pageRef -TotalPages $totalPages New-ScoutActFirstSlide -Shell $shell -Gaps $gaps -PageCounter $pageRef -TotalPages $totalPages # Figures follow the priority slide and precede the tables: a chart is an argument about the # whole run, so it belongs where the run is being characterised, not among the detail. New-ScoutFigureSlides -Shell $shell -Figures $figures -PageCounter $pageRef -TotalPages $totalPages New-ScoutAreaTableSlides -Shell $shell -Areas $areas -PageCounter $pageRef -TotalPages $totalPages -MaxPages $areaPages New-ScoutGapsSlides -Shell $shell -Gaps $gaps -PageCounter $pageRef -TotalPages $totalPages -MaxGaps ($gapPages * 10) New-ScoutManualSlide -Shell $shell -Manual $manual -PageCounter $pageRef -TotalPages $totalPages New-ScoutNextStepsSlide -Shell $shell -PageCounter $pageRef -TotalPages $totalPages # ---- wire the presentation-level lists and save ---- $slideMasterIdList = New-ScoutEl "$Script:PresNs.SlideMasterIdList" $smId = New-ScoutEl "$Script:PresNs.SlideMasterId" $smId.Id = [uint32]2147483648 $smId.RelationshipId = $shell.PresPart.GetIdOfPart($shell.MasterPart) $slideMasterIdList.Append($smId) $slideSize = New-ScoutEl "$Script:PresNs.SlideSize" $slideSize.Cx = [int32](ScoutEmu32 $Script:SlideWIn) $slideSize.Cy = [int32](ScoutEmu32 $Script:SlideHIn) $slideSize.Type = [DocumentFormat.OpenXml.Presentation.SlideSizeValues]::Screen16x9 $notesSize = New-ScoutEl "$Script:PresNs.NotesSize" $notesSize.Cx = [int32](ScoutEmu32 7.5) $notesSize.Cy = [int32](ScoutEmu32 10) $shell.PresPart.Presentation.Append($slideMasterIdList, $shell.SlideIdList, $slideSize, $notesSize, (New-ScoutEl "$Script:PresNs.DefaultTextStyle")) $shell.PresPart.Presentation.Save() $shell.Doc.Dispose() return $outFile } |