Table of Contents

  1. Opening the Word Document, Writing Some Text, Using Styles
  2. Adding and Manipulating Tables, Adding Images
  3. Document Properties, Table of Contents, Changing Page Properties
  4. Moving around the Document

In the first part of this series, I talked about the first steps you should take if you want to start creating and manipulating Word documents with Powershell - you can find the link to that blog above - and here we’re going to look at what is likely to be the next stage for you; adding and manipulating tables and images.

Tables

Tables is one of the first things I looked at when learning how to create Word documents in Powershell, as I needed to be able to tabularise data collected from a number of APIs as well as fill in cells of pre-existing tables. We’ll be talking about both, so that should cover most use cases!

I’m going to be using a dummy set of data for the purposes of this tutorial; the below script will create an object with multiple entries that we’ll be able to use to populate our table.

$randomObj = for ($i = 1; $i -lt 10; $i++) {
    New-Object PsObject -Property @{
        "index" = $i
        "name" = "Object $i"
        "value" = "Value $i"
    }
}

Creating a new table

I’m going to assume that you’ve already followed Part 1, and so have the script at the end to start working with!

The first thing we need to do is define our table - how many columns, how many rows, what style to use, and what name to give it.

Rows

We’ll usually need N+1 rows, where N is the number of rows in our object. I don’t need to know how many this is ahead of time, we’ll just count the number of objects and add one to it:

$count = $randomObj.Count
$rowsNo = $count + 1

Columns

You’ll need to know and define this in advance; in my case, we’ve got three columns

$colsNo = 3

And with those we can create the table:

$table = $text.Tables.Add(
    $text.Range, $rowsNo, $colsNo
)

#$text.Range is the current position of the text 'selector' object within the Word document; this is just creating it wherever we've left our imaginary cursor

Style and Title

Once we’ve created the table, we can then start applying and modifying it’s properties - for the purposes of this blob I’m only going to modify it’s style and title (that will be important later):

$table.style = "Plain Table 3" #this is a built in style; you may have custom styles available, and these can also be referenced by their name
$table.title = "randomObjectTable"

Content

I can now start adding content! My table is going to have a header row, so I can define that as a static object:


$table.cell(1,1).range.text = "Index"
$table.cell(1,2).range.text = "Name"
$table.cell(1,3).range.text = "Value"

#I can also apply a style to these cells, to differentiate the header row from the rest of the table

$table.cell(1,1).range.style = "Strong"
$table.cell(1,2).range.style = "Strong"
$table.cell(1,3).range.style = "Strong"

But I want to be able to iterate through my object and add the data iteratively; so we need to define a variable to track what row we’re on, and move through the document using a foreach-object loop:

$r = 2 #we start at 2 as we're going to be populating our table from Row 2

$randomObj | foreach-object {

    $table.cell($r,1).range.text = $($_ | Select-Object -expandProperty index).ToString() #All items added to Word must be a string; you will get an error if you try to enter an Int or other object type
    $table.cell($r,2).range.text = $_ | Select-Object -expandProperty name
    $table.cell($r,3).range.text = $_ | Select-Object -expandProperty value

    $r++ #we need to iterate our $r object by 1 each time to move onto the next row
}

Finishing up the table

Once you’ve entered all your data, you’ll want to format your table to fit your page and also then move the ‘cursor’ onto the next line - this took a little trial and error on my part, but eventually I got to this that has proven effective in many different scenarios:

$table.AutoFitBehavior(2) #This autofits the table to the window; if you want to autofit to content, use 1 instead
$range = $doc.Range($table.Range.End, $table.Range.End) #this defines a range within the document at the end of the table
$range.select() #this moves the selection/cursor to the end of the table
$text = $word.selection #this sets our $text object to now be in the same place as $range.select()

$text.TypeParagraph() #this creates a new paragraph for new text or anything else!

Which should leave you with a table like this:

Generated Table

You can of course do plenty more with the formatting; alignment, font size, font colour etc. but I’ll leave that for a future blog!

Modifying an existing table

Lets say you’re working with a templated document, and you have particular fields within a table that you need to update. Or you’ve predefined a table for positioning or formatting, and you now want to add data to it. Or you’ve got more data you need to add to an existing table. This next section is going to show you how to reference an existing table and make modifications to it, and we’re going to use the table we just created.

Referencing the table

First, lets get a list of all the tables in our document and then reference the object that is the table we’ve just created:

$tables = $doc.Tables
$randomObjTable = $tables | Where-Object {$_.Title -eq "randomObjectTable"}

Modifying the table

We can now start manipulating it; my scenario is that I’m going to have some new data that I want to add to the table:

$randomObjCount = $randomObj.count #we'll want this later so that we can count how many rows have been added
$randomObj += New-Object PsObject -Property @{
    "index" = 10
    "name" = "Object 10"
    "value" = "Value 10"
}

We’re then going to define the number of rows we need to add, and add them to the table; in my case it’s only one but I’m going to write it as if there were multiple:

$rowsToAdd = $($randomObj.count) - $randomObjCount
for ($s = 1; $s -le $rowsToAdd; $s++) {
    $randomObjTable.Rows.Add() | Out-Null
}

Which adds a new empty row (or rows) to the table:

A New Empty Row

Adding data to the table

From which, we are then able to add data - the first point will be to determine our start row, and then iterate from there in another for loop:

$r = $randomObjCount + 1 + 1 #we take the count of our object before we added new data, add 1 for the header row, and then add 1 for the new row

for ($r; $r -le $($randomObj.count + 1); $r++){
    $index = $r - 2 #we need to define our index value for referencing that specific item in our object
    $randomObjTable.cell($r, 1).range.text = $($randomObj[$index] | Select-Object -expandProperty index).ToString()
    $randomObjTable.cell($r, 2).range.text = $randomObj[$index] | Select-Object -expandProperty name
    $randomObjTable.cell($r, 3).range.text = $randomObj[$index] | Select-Object -expandProperty value
}

Which then leaves you with the data inserted!

New Row Filled In

Images

So you’ve added your tables, but now you want to impress with including some images; perhaps pulled automatically from the internet even? For the purposes of this I’m going to use a local file, but the possiblities are endless!

The image I’m going to add, courtesy of my friendly Copilot, is this one:

AI Generated Image

Very topical, I’m sure you’ll agree. And it’s really quite simple to then add the image to the document:


$imagePath = "C:\Temp\copilotImageForDemo.jpeg"

$insertedImage = $text.InlineShapes.AddPicture($imagePath, $false, $true)
#$false means "Do not link to the file", so it embeds the image
#$true means "Save with Document"

And that’s it! Running it immediately after everything we’ve already done should leave you with this:

The inserted image

But that’s pretty big, right - I don’t need it to be that big. No problem, all we need to do is modify the Width and Height of the image using the $insertedImage object we’ve created:


#I want to reduce the size by about 50%, so th easiest way to do that is using the existing values

$newWidth = $insertedImage.width / 2
$newHeight = $insertedImage.height / 2

$insertedImage.width = $newWidth
$insertedImage.height = $newHeight

Much better:

Resized Image

And that’s it! You’ve got the starting point of being able to manipulate tables and images now.

In our next part, we’ll look at modifying the document properties, updating page attributes like orientation, and utilising a table of contents. For now, find below our complete script for everything up until this point:

$randomObj = for ($i = 1; $i -lt 10; $i++) {
    New-Object PsObject -Property @{
        "index" = $i
        "name" = "Object $i"
        "value" = "Value $i"
    }
}

$word = New-Object -ComObject Word.Application
$word.visible = $true

#If you're starting with a blank document
$doc = $word.Documents.Add()

$text = $word.Selection

$text.style = "Heading 1"
$text.TypeText("This is Heading 1")
$text.TypeParagraph()

$text.style = "Body Text"
$text.TypeText("This is a section of text immediately under Heading 1. This might be where you add some dynamically generated information")
$text.TypeParagraph()

$text.style = "Heading 2"
$text.TypeText("This is Heading 2")
$text.TypeParagraph()

$count = $randomObj.Count
$rowsNo = $count + 1
$colsNo = 3

$table = $text.Tables.Add(
    $text.Range, $rowsNo, $colsNo
)

#$text.Range is the current position of the text 'selector' object within the Word document; this is just creating it wherever we've left our imaginary cursor

$table.style = "Plain Table 3" #this is a built in style; you may have custom styles available, and these can also be referenced by their name
$table.title = "randomObjectTable"

$table.cell(1,1).range.text = "Index"
$table.cell(1,2).range.text = "Name"
$table.cell(1,3).range.text = "Value"

#I can also apply a style to these cells, to differentiate the header row from the rest of the table

$table.cell(1,1).range.style = "Strong"
$table.cell(1,2).range.style = "Strong"
$table.cell(1,3).range.style = "Strong"

$r = 2 #we start at 2 as we're going to be populating our table from Row 2

$randomObj | foreach-object {

    $table.cell($r,1).range.text = $($_ | Select-Object -expandProperty index).ToString() #All items added to Word must be a string; you will get an error if you try to enter an Int or other object type
    $table.cell($r,2).range.text = $($_ | Select-Object -expandProperty name)
    $table.cell($r,3).range.text = $($_ | Select-Object -expandProperty value)

    $r++ #we need to iterate our $r object by 1 each time to move onto the next row
}

$table.AutoFitBehavior(2) #This autofits the table to the window; if you want to autofit to content, use 1 instead
$range = $doc.Range($table.Range.End, $table.Range.End) #this defines a range within the document at the end of the table
$range.select() #this moves the selection/cursor to the end of the table
$text = $word.selection #this sets our $text object to now be in the same place as $range.select()

$text.TypeParagraph() #this creates a new paragraph for new text or anything else!


$tables = $doc.Tables
$randomObjTable = $tables | Where-Object {$_.Title -eq "randomObjectTable"}

$randomObjCount = $randomObj.count #we'll want this later so that we can count how many rows have been added
$randomObj += New-Object PsObject -Property @{
    "index" = 10
    "name" = "Object 10"
    "value" = "Value 10"
}

$rowsToAdd = $($randomObj.count) - $randomObjCount
for ($s = 1; $s -le $rowsToAdd; $s++) {
    $randomObjTable.Rows.Add() | Out-Null
}

$r = $randomObjCount + 1 + 1 #we take the count of our object before we added new data, add 1 for the header row, and then add 1 for the new row

for ($r; $r -le $($randomObj.count + 1); $r++){
    $index = $r - 2 #we need to define our index value for referencing that specific item in our object
    $randomObjTable.cell($r, 1).range.text = $($randomObj[$index] | Select-Object -expandProperty index).ToString()
    $randomObjTable.cell($r, 2).range.text = $randomObj[$index] | Select-Object -expandProperty name
    $randomObjTable.cell($r, 3).range.text = $randomObj[$index] | Select-Object -expandProperty value
}

$imagePath = "C:\Temp\copilotImageForDemo.jpeg"

$insertedImage = $text.InlineShapes.AddPicture($imagePath, $false, $true)
#$false means "Do not link to the file", so it embeds the image
#$true means "Save with Document"

#I want to reduce the size by about 50%, so th easiest way to do that is using the existing values

$newWidth = $insertedImage.width / 2
$newHeight = $insertedImage.height / 2

$insertedImage.width = $newWidth
$insertedImage.height = $newHeight

$doc.SaveAs([Path To Save File To])
$doc.Close
$word.Quit