{"id":11900,"date":"2026-01-06T16:41:24","date_gmt":"2026-01-06T21:41:24","guid":{"rendered":"https:\/\/www.rushworth.us\/lisa\/?p=11900"},"modified":"2026-01-06T16:41:25","modified_gmt":"2026-01-06T21:41:25","slug":"did-you-know-powershell-can-create-visio-diagrams","status":"publish","type":"post","link":"https:\/\/www.rushworth.us\/lisa\/?p=11900","title":{"rendered":"Did you know &#8230; Powershell can create Visio diagrams!?!"},"content":{"rendered":"\n<p>I had to create a number of Visio diagrams for a new project. Since Blender has a Python API, I wondered if I could do something similar with Visio. There does appear to be an <a href=\"https:\/\/pypi.org\/project\/vsdx\/\" data-type=\"link\" data-id=\"https:\/\/pypi.org\/project\/vsdx\/\">VSDX library for Python<\/a>, I also found that Powershell can just control the Visio instance on my laptop. <\/p>\n\n\n\n<p>This is a demo creating a diagram for a simple web server with a database back end. You can, however, use any stencils and make more complicated diagrams. The lines aren&#8217;t great &#8212; part of my Visio diagramming process is moving things around to optimize placement to avoid overlapping and confusing lines. The programmatic approach doesn&#8217;t do that, but it gets everything in the diagram. You can then move them as needed. <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: powershell; title: ; notranslate\" title=\"\">\n# Sample Visio diagram: Firewall -&gt; Load Balancer -&gt; Web Servers -&gt; Database\n# Auto-discovers stencils\n# Works on Windows PowerShell 5.x\n\n$ErrorActionPreference = &quot;Stop&quot;\n\n# Output\n$docName = &quot;WebApp-LB-Firewall-DB.vsdx&quot;\n$outPath = Join-Path $HOME &quot;Documents\\$docName&quot;\n\n# Start Visio\n$visio = New-Object -ComObject Visio.Application\n$visio.Visible = $true\n\n# New document\/page\n$doc = $visio.Documents.Add(&quot;&quot;)\n$page = $visio.ActivePage\n$page.Name = &quot;Architecture&quot;\n$page.PageSheet.CellsU(&quot;PageWidth&quot;).ResultIU  = 22.0\n$page.PageSheet.CellsU(&quot;PageHeight&quot;).ResultIU = 14.0\n\n# -------------------------------\n# Stencil discovery and loading\n# -------------------------------\n\n$searchRoots = @(\n    &quot;$env:PROGRAMFILES\\Microsoft Office\\root\\Office16\\Visio Content&quot;,\n    &quot;$env:PROGRAMFILES\\Microsoft Office\\root\\Office16\\Visio Content\\1033&quot;,\n    &quot;$env:ProgramFiles(x86)\\Microsoft Office\\root\\Office16\\Visio Content&quot;,\n    &quot;$env:ProgramFiles(x86)\\Microsoft Office\\root\\Office16\\Visio Content\\1033&quot;,\n    &quot;$env:PROGRAMFILES\\Microsoft Office\\root\\Office15\\Visio Content&quot;,\n    &quot;$env:ProgramFiles(x86)\\Microsoft Office\\root\\Office15\\Visio Content&quot;,\n    &quot;$env:PROGRAMFILES\\Microsoft&quot;,\n    &quot;$env:ProgramFiles(x86)\\Microsoft&quot;,\n    &quot;$env:PROGRAMFILES&quot;,\n    &quot;$env:ProgramFiles(x86)&quot;\n) | Where-Object { Test-Path $_ }\n\n# Keywords to select useful stencils (filename match, case-insensitive)\n$stencilKeywords = @(&quot;network&quot;,&quot;server&quot;,&quot;compute&quot;,&quot;computer&quot;,&quot;azure&quot;,&quot;cloud&quot;,&quot;firewall&quot;,&quot;security&quot;,&quot;database&quot;,&quot;sql&quot;,&quot;load&quot;,&quot;balancer&quot;,&quot;web&quot;,&quot;iis&quot;)\n\nfunction Find-StencilFiles {\n    param(&#x5B;string&#x5B;]]$roots, &#x5B;string&#x5B;]]$keywords)\n    $results = @()\n    foreach ($root in $roots) {\n        try {\n            Get-ChildItem -Path $root -Filter *.vssx -Recurse -ErrorAction SilentlyContinue | ForEach-Object {\n                $fname = $_.Name.ToLower()\n                foreach ($kw in $keywords) {\n                    if ($fname -match $kw) { $results += $_.FullName; break }\n                }\n            }\n        } catch { }\n    }\n    $results | Select-Object -Unique\n}\n\nfunction Load-Stencils {\n    param(&#x5B;string&#x5B;]]$files)\n    $loaded = @()\n    foreach ($file in $files) {\n        try {\n            Write-Host &quot;Loading stencil: $file&quot;\n            $loaded += $visio.Documents.OpenEx($file, 64) # read-only\n        } catch {\n            Write-Warning &quot;Could not load stencil: $file&quot;\n        }\n    }\n    foreach ($docX in $visio.Documents) {\n        if ($docX.FullName -ne $doc.FullName) { $loaded += $docX }\n    }\n    $loaded | Sort-Object FullName -Unique\n}\n\n$files = Find-StencilFiles -roots $searchRoots -keywords $stencilKeywords\n$stencils = Load-Stencils -files $files\n\nif (!$stencils -or $stencils.Count -eq 0) {\n    Write-Warning &quot;No stencil files loaded automatically. Fallback rectangles will be used.&quot;\n} else {\n    Write-Host &quot;`nLoaded stencils:&quot; -ForegroundColor Cyan\n    foreach ($s in $stencils) { Write-Host &quot; - $($s.FullName)&quot; }\n}\n\n# -------------------------------\n# Master selection helpers\n# -------------------------------\n\nfunction List-Masters {\n    foreach ($st in $stencils) {\n        Write-Host (&quot;Stencil\/Doc: {0}&quot; -f $st.Name) -ForegroundColor Cyan\n        foreach ($m in $st.Masters) {\n            Write-Host (&quot;  - {0} (NameU: {1})&quot; -f $m.Name, $m.NameU)\n        }\n    }\n}\n\nfunction Get-MasterByPattern(&#x5B;string&#x5B;]]$patterns) {\n    foreach ($st in $stencils) {\n        foreach ($m in $st.Masters) {\n            foreach ($p in $patterns) {\n                if ($m.NameU -match $p -or $m.Name -match $p) {\n                    Write-Host (&quot;Selected master &#039;{0}&#039; from &#039;{1}&#039; for pattern &#039;{2}&#039;&quot; -f $m.Name, $st.Name, $p) -ForegroundColor Green\n                    return $m\n                }\n            }\n        }\n    }\n    return $null\n}\n\n# Drop master centered at x,y; keep default size; label it\nfunction Add-Device(&#x5B;double]$x,&#x5B;double]$y,&#x5B;string]$label,&#x5B;string&#x5B;]]$patterns,&#x5B;double]$fontSize=10) {\n    $m = Get-MasterByPattern $patterns\n    if ($null -eq $m) {\n        Write-Warning (&quot;No master matched patterns: {0}. Using fallback rectangle.&quot; -f ($patterns -join &quot;, &quot;))\n        $w = 2.0; $h = 1.2\n        $shape = $page.DrawRectangle($x - ($w\/2), $y - ($h\/2), $x + ($w\/2), $y + ($h\/2))\n    } else {\n        $shape = $page.Drop($m, $x, $y)\n    }\n    $shape.Text = $label\n    $shape.CellsU(&quot;Char.Size&quot;).FormulaU = &quot;$fontSize pt&quot;\n    return $shape\n}\n\n# Simple transparent containers (thin gray outline; sent behind shapes)\nfunction Add-Container(&#x5B;double]$x,&#x5B;double]$y,&#x5B;double]$w,&#x5B;double]$h,&#x5B;string]$text) {\n    $shape = $page.DrawRectangle($x, $y, $x + $w, $y + $h)\n    $shape.CellsU(&quot;LineColor&quot;).FormulaU = &quot;RGB(180,180,180)&quot;\n    $shape.CellsU(&quot;LineWeight&quot;).FormulaU = &quot;1 pt&quot;\n    $shape.CellsU(&quot;FillForegnd&quot;).FormulaU = &quot;RGB(255,255,255)&quot;\n    $shape.CellsU(&quot;FillForegndTrans&quot;).ResultIU = 1.0\n    $shape.Text = $text\n    $shape.CellsU(&quot;Char.Size&quot;).FormulaU = &quot;12 pt&quot;\n    try { $shape.SendToBack() } catch {}\n    return $shape\n}\n\n# Connector\nfunction Connect($fromShape,$toShape,&#x5B;string]$text=&quot;&quot;) {\n    $conn = $page.Drop($visio.Application.ConnectorToolDataObject, 0, 0)\n    $conn.CellsU(&quot;LineColor&quot;).FormulaU = &quot;RGB(60,60,60)&quot;\n    $conn.CellsU(&quot;LineWeight&quot;).FormulaU = &quot;0.75 pt&quot;\n    $fromShape.AutoConnect($toShape, 0, $conn)\n    if ($text) { $conn.Text = $text }\n    return $conn\n}\n\n# -------------------------------\n# Diagram content\n# -------------------------------\n\n# Title\n$title = $page.DrawRectangle(1.0, 13.4, 21.0, 13.9)\n$title.Text = &quot;Web App Architecture: Firewall -&gt; Load Balancer -&gt; Web Servers -&gt; Database&quot;\n$title.CellsU(&quot;Char.Size&quot;).FormulaU = &quot;14 pt&quot;\n\n# Patterns for official icons (broad to match common stencils)\n$patFirewall    = @(&quot;Firewall|Security|Shield|Azure.*Firewall&quot;)\n$patLoadBalancer= @(&quot;Load.*Balancer|Application.*Gateway|LB|Azure.*Load.*Balancer&quot;)\n$patWebServer   = @(&quot;Web.*Server|IIS|Server(?! Rack)|Computer|Windows.*Server&quot;)\n$patDatabase    = @(&quot;Database|SQL|Azure.*SQL|DB|Cylinder&quot;)\n\n# Containers (optional zones)\n$dmz     = Add-Container 1.0 10.8 20.0 2.0 &quot;DMZ (Edge\/Ingress)&quot;\n$webtier = Add-Container 4.0 6.8 14.0 3.2 &quot;Web Tier&quot;\n$dbtier  = Add-Container 8.0 3.5 10.0 2.8 &quot;Database Tier&quot;\n$clients = Add-Container 1.0 1.0 6.0 2.2 &quot;Clients&quot;\n\n# Devices (kept at native size; spaced widely)\n# Edge\/Ingress\n$fw      = Add-Device 3.0 11.8 &quot;Firewall&quot; $patFirewall 10\n$lb      = Add-Device 8.0 11.8 &quot;Load Balancer&quot; $patLoadBalancer 10\n\n# Web servers (pair)\n$web1    = Add-Device 9.5 8.0 &quot;Web Server 1\\nIIS&quot; $patWebServer 10\n$web2    = Add-Device 13.5 8.0 &quot;Web Server 2\\nIIS&quot; $patWebServer 10\n\n# Database\n$db      = Add-Device 13.0 4.6 &quot;Database\\nSQL&quot; $patDatabase 10\n\n# Clients\n$client1 = Add-Device 2.0 1.8 &quot;Client\\nPC&quot; @(&quot;Desktop|PC|Computer|Laptop&quot;) 10\n$client2 = Add-Device 5.0 1.8 &quot;Client\\nServer&quot; @(&quot;Server(?! Rack)|Windows.*Server|Computer&quot;) 10\n\n# Connectors (flow: clients -&gt; firewall -&gt; LB -&gt; web servers -&gt; database)\nConnect $client1 $fw &quot;HTTPS&quot;\nConnect $client2 $fw &quot;HTTPS&quot;\nConnect $fw $lb &quot;Allow: 443&quot;\nConnect $lb $web1 &quot;HTTP\/HTTPS&quot;\nConnect $lb $web2 &quot;HTTP\/HTTPS&quot;\nConnect $web1 $db &quot;SQL (1433\/Encrypted)&quot;\nConnect $web2 $db &quot;SQL (1433\/Encrypted)&quot;\n\n# Save\n$doc.SaveAs($outPath)\nWrite-Host &quot;Saved Visio to: $outPath&quot;\n<\/pre><\/div>","protected":false},"excerpt":{"rendered":"<p>I had to create a number of Visio diagrams for a new project. Since Blender has a Python API, I wondered if I could do something similar with Visio. There does appear to be an VSDX library for Python, I also found that Powershell can just control the Visio instance on my laptop. This is &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[30],"tags":[622,825],"class_list":["post-11900","post","type-post","status-publish","format-standard","hentry","category-system-administration","tag-powershell","tag-visio"],"_links":{"self":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/11900","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11900"}],"version-history":[{"count":1,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/11900\/revisions"}],"predecessor-version":[{"id":11901,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/11900\/revisions\/11901"}],"wp:attachment":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11900"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11900"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11900"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}