How to set parameters for SaveAs() dialog in Word.Application? - powershell

I want to save a Word document as PDF from a PowerShell script.
The following code works for me.
$Word = New-Object -ComObject Word.Application
$Doc = $Word.Documents.Open("C:\TEMP\WORD.DOCX")
$Name = ($Doc.Fullname).Replace("DOCX", "PDF")
$result = $Doc.SaveAs([ref] $Name, [ref] 17)
$Doc.Close()
echo "Saved to $Name"
The produced PDF is a PDF/A though.
When I save the document manually then I can set the option "PDF/A compliant" in a dialog which pops up.
How can I change this format specific option via PowerShell?
The pictures explain perhaps better what I'm trying.

The only way I know of is by using the ExportAsFixedFormat function instead of SaveAs.
$Word = New-Object -ComObject Word.Application
$Doc = $Word.Documents.Open("C:\TEMP\WORD.DOCX")
$Name = [System.IO.Path]::ChangeExtension($Doc.Fullname, "PDF")
# Use ExportAsFixedFormat function.
# See: https://learn.microsoft.com/en-us/office/vba/api/word.document.exportasfixedformat
# Parameters:
# OutputFileName, ExportFormat, OpenAfterExport, OptimizeFor, Range, From
# To, Item, IncludeDocProps, KeepIRM, CreateBookmarks, DocStructureTags
# BitmapMissingFonts, UseISO19005_1
# The last parameter 'UseISO19005_1' saves as PDF/A Compliant
$result = $Doc.ExportAsFixedFormat(
$Name,
[Microsoft.Office.Interop.Word.WdExportFormat]::wdExportFormatPDF,
$false,
[Microsoft.Office.Interop.Word.WdExportOptimizeFor]::wdExportOptimizeForOnScreen,
[Microsoft.Office.Interop.Word.WdExportRange]::wdExportAllDocument,
0,
0,
[Microsoft.Office.Interop.Word.WdExportItem]::wdExportDocumentContent,
$true,
$true,
[Microsoft.Office.Interop.Word.WdExportCreateBookmarks]::wdExportCreateWordBookmarks,
$true,
$false,
$true
)
$Doc.Close()
# clean up Com object after use
$Word.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Word) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

Related

Removing passwords from .Docx files using Powershell

I'm very new to Powershell and been banging my head against this for a while, hopefully someone can point me towards where I am going wrong. I am trying to use Powershell to remove the opening passwords from multiple .docx files in a folder. I can get it to change the password to something else but cannot get it to remove entirely, the part in Bold below is where I am getting tripped up and the error code details are at the bottom, appreciate any help with this!
$path = ("FilePath")
$passwd = ("CurrentPassword")
$counter=1
$WordObj = New-Object -ComObject Word.Application
foreach ($file in $count=Get-ChildItem $path -Filter \*.docx) {
$WordObj.Visible = $true
$WordDoc = $[WordObj.Documents.Open](https://WordObj.Documents.Open)
($file.FullName, $null, $false, $null, $passwd)
$WordDoc.Activate()
$WordDoc.Password=$null
$WordDoc.Close()
Write-Host("Finished: "+$counter+" of "+$count.Length)
$counter++
}
$WordObj.Application.Quit()
**Error details -** Object reference not set to an instance of an object. At line: 14 char: 5
\+$WordDoc.Password=$Null
\+Category info: Operations Stopped: (:) \[\], NullReferenceException
\+FullyQualifiedErrorId: System.NullReferenceException
I got an answer elsewhere to try using .unprotect instead but not sure how to insert this into my code!
$path = 'X:\TheFolderWhereTheProtectedDocumentsAre'
$passwd = 'CurrentPassword'
$counter = 0
$WordObj = New-Object -ComObject Word.Application
$WordObj.Visible = $false
# get the .docx files. Make sure this is an array using #()
$documentFiles = #(Get-ChildItem -Path $path -Filter '*.docx' -File)
foreach ($file in $documentFiles) {
try {
# add password twice, first for the document, next for the documents template
$WordDoc = $WordObj.Documents.Open($file.FullName, $null, $false, $null, $passwd, $passwd)
$WordDoc.Activate()
$WordDoc.Password = $null
$WordDoc.Close(-1) # wdSaveChanges, see https://learn.microsoft.com/en-us/office/vba/api/word.wdsaveoptions
$counter++
}
catch {
Write-Warning "Could not open file $($file.FullName):`r`n$($_.Exception.Message)"
}
}
Write-Host "Finished: $counter documents of $($documentFiles.Count)"
# quit Word and dispose of the used COM objects in memory
$WordObj.Quit()
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WordDoc)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WordObj)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

BrowseForFolders doesn't display on top

I'm writing PS Script and following block of code shows dialogbox below windows forms gui.
$btn1 = New-Object Windows.Forms.Button
$btn1.Text = "Wybierz folder projektowy"
$btn1.Location = New-Object System.Drawing.Point(170,140)
$btn1.Size = New-Object System.Drawing.Size(160,20)
$btn1.add_Click({
function Select-Folder($message='Select a folder', $path = 0) {
$object = New-Object -comObject Shell.Application
$object.topmost=$true
$folder = $object.BrowseForFolder(0, $message, 0, $path)
if ($folder -ne $null) {
$folder.self.Path
}
}
$folderPath = Select-Folder 'Select the folder where the move scripts reside'
If ($folderPath) {
Set-Content -Path "C:\Projekty\logs\temp_path.txt" -Value $folderPath.ToString() -Encoding Unicode
write-host $folderPath
get-content -Path "C:\Projekty\logs\temp_path.txt"
}
Else { Write-Host 'I do not have a folder path' }
})
$form_acl.Controls.Add($btn1)
Is there any way to make it display on top?
Here's screenshot of a problem:
You can try this alternative instead:
function Select-Folder {
[CmdletBinding()]
param (
# sets the descriptive text displayed above the tree view control in the dialog box
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Please select a directory.",
# sets the (pre)selected path
[Parameter(Mandatory=$false, Position=1)]
[string]$InitialDirectory,
# sets the root folder where the browsing starts from
[Parameter(Mandatory=$false)]
[System.Environment+SpecialFolder]$RootFolder = [System.Environment+SpecialFolder]::Desktop,
# sets a value indicating whether the 'New Folder' button appears in the folder browser dialog box
[switch]$ShowNewFolderButton
)
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = $Message
$dialog.SelectedPath = $InitialDirectory
$dialog.RootFolder = $RootFolder
$dialog.ShowNewFolderButton = $ShowNewFolderButton.IsPresent
$selected = $null
# force the dialog TopMost:
# because the owning window will not be used after the dialog has been closed,
# you can simply create a new form inside the method call.
$result = $dialog.ShowDialog((New-Object System.Windows.Forms.Form -Property #{TopMost = $true; TopLevel = $true}))
if ($result -eq [Windows.Forms.DialogResult]::OK){
$selected = $dialog.SelectedPath
}
# clear the FolderBrowserDialog from memory
$dialog.Dispose()
# return the selected folder
$selected
}
Select-Folder -Message 'Select the folder where the move scripts reside' -ShowNewFolderButton
Theo's helpful answer shows an alternative, WinForms-based way to invoke a folder-browsing dialog, via the System.Windows.Forms.FolderBrowserDialog class.
However, it seems that all that is missing from your original approach is to pass your form's window handle (hWND, .Handle) as the first argument to the Shell.Application COM object's .BrowseForFolder() method, which makes it the dialog's owner window and therefore shows the dialog on top of it - even if the form itself has the .TopMost property set:
$folder = (New-Object -ComObject Shell.Application).BrowseForFolder(
$form_acl.Handle, # Pass your form's window handle to make it the owner window
$message,
0,
$path)
Here's a simplified, self-contained example (requires PSv5+, but can be adapted to earlier versions):
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create a sample topmost form with a single
# button that invokes a folder-browsing dialog.
($form = [Form] #{
Text = "Topmost Form"
Size = [Size]::new(300, 100)
TopMost = $true # Make the form topmost.
StartPosition = 'CenterScreen'
}).Controls.AddRange(#(
($folderBrowseButton = [Button] #{
Location = [Point]::new(70, 20)
Size = [Size]::new(160,30)
Text = 'Browse for Folder'
})
))
$folderBrowseButton.add_Click({
$path = 'C:\'
# IMPORTANT: Pass $form.Handle, the form's window handle (HWND) as the first argument, which
# makes the form the owning window, ensuring that the dialog is displayed on
# top - even if the form itself is set to topmost.
$folder = (New-Object -ComObject Shell.Application).BrowseForFolder(
$form.Handle,
'Pick a target folder:',
0, # Options
$path # Starting directory path.
)
if ($null -ne $folder) {
Write-Verbose -vb ('Folder picked: ' + $folder.self.Path)
} else {
Write-Verbose -vb 'Folder browsing canceled.'
}
})
$null = $form.ShowDialog()

Replace Multiple words in MSWord using Powershell

I am trying to use powershell to replace multiple lines in a word document without having to save and close after every change. right now I have:
$objWord = New-Object -comobject Word.Application
function findAndReplace
{
$objDoc = $objWord.Documents.Open("C:\temp\test.docx")
$objWord.Visible = $false
$objSelection = $objWord.Selection
$a = $objSelection.Find.Execute($FindText, $false, $true, $False, $False, $False, $true, 1, $False, $ReplaceWith)
$objDoc.Save()
$objDoc.close()
}
############DEVICE DETAILS#############
$FindText = "USERID"
$ReplaceWith = $AssociateIDnum
findAndReplace
$FindText = "CONTACTNUMBER"
$ReplaceWith = $Phone
findAndReplace
Using this, I made the function that runs those commands, and I change the $findtext and $replacetext on each instance of a new word, then run the function each time.
With this method, the Word Doc opens and closes nearly 25 times to write each new word replacement.
Is there a way I can make a function or loop to make it change the $findtext
and $replacewith variable each time?
I am relatively new to powershell but I have been learning some of it on my own.
Try using a list of objects as a parameter to the function, something like that :
$objWord = New-Object -comobject Word.Application
function findAndReplace ($todoObjs)
{
$objDoc = $objWord.Documents.Open("C:\temp\test.docx")
$objWord.Visible = $false
$objSelection = $objWord.Selection
foreach ($todoObj in $todoObjs)
{
$a = $objSelection.Find.Execute($($todoObj.FIND), $false, $true, $False, $False, $False, $true, 1, $False, $($todoObj.REPLACE))
}
$objDoc.Save()
$objDoc.close()
}
############DEVICE DETAILS#############
$todo = #"
FIND,REPLACE
toto,titi
tutu,tata
"#
$todoObjs = ConvertFrom-Csv $todo
findAndReplace $todoObjs

Powershell word header replace

I want to replace header in word document.
$pth = "d:\test\test.docx"
$objWord = New-Object -ComObject word.application
$objWord.Visible = $True
$objDoc = $objWord.Documents.Open($pth)
$objSelection = $objWord.Selection
$Section = $objDoc.Sections.Item(1)
$header = $Section.Headers.Item(1)
This return me a plain text:
Write-Host $header.Range.Text
But my header have an image and table. Can i replace string in header without destroying header? I replace strings in word document and works great. My only problem is header.
Link to example Word document header below.
http://zapodaj.net/223c522426648.png.html
Try this:
$replaceWith = "New Text !"
$replace = [Microsoft.Office.Interop.Word.WdReplace]::wdReplaceAll
$findWrap = [Microsoft.Office.Interop.Word.WdFindWrap]::wdFindContinue
$find = $header.Range.find
$find.Execute($header.Range.Text,
$false, #match case
$false, #match whole word
$false, #match wildcards
$false, #match soundslike
$false, #match all word forms
$true, #forward
$findWrap,
$null, #format
$replaceWith,
$replace)
The pictures and other tables, should remain untouched.
I don't know solution in powershell but I use VBA runed from powershell.
// code is changed so if it don't work let me know
$objWord = New-Object -ComObject word.application
$objWord.Visible = $True # don't have to be true
$pathToFile = "d:\Delivery_Templates\filename.docx" #path to your file
$objDoc = $objWord.Documents.Open(pathToFile )
$objSelection = $objWord.Selection
$objWord.Run('myReplace', [ref] $currentVersion); # myReplace - macro name, currentVersion - macro parameter

Powershell with Word CheckSpelling and selected dictionary

I would like to spell check some strings using the Microsoft Word API in Powershell and a specific dictionary ("English (US)").
I use the following code to do the checking but it does not seem to take into account the dictionary I want. Any ideas what is wrong? Also, command "New-Object -COM Word.Dictionary" seems to fail.
$word = New-Object -COM Word.Application
$dictionary = New-Object -COM Word.Dictionary
foreach ($language in $word.Languages) {
if ($language.Name.Contains("English (US)")) {
$dictionary = $language.ActiveSpellingDictionary;
break;
}
}
Write-Host $dictionary.Name
$check = $word.CheckSpelling("Color", [ref]$null, [ref]$null, [ref]$dictionary)
if(!$check) {
Write-Host "Spelling Error!"
}
$word.quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($word) | Out-Null
Remove-Variable word
The COMobject word.dictionary does not exist (at least not on my machine), here is what worked for me in the short test i did:
$dic = New-Object -COM Scripting.Dictionary #credits to MickyB
$w = New-Object -COM Word.Application
$w.Languages | % {if($_.Name -eq "English (US)"){$dic=$_.ActiveSpellingDictionary}}
$w.checkSpelling("Color", [ref]$null, [ref]$null, [ref]$dic)
Another possibility in addition to Paul's:
$dictionary = New-Object -COM Scripting.Dictionary
I was experiencing the same problem, that the Word.Application.checkSpelling() method seems to ignore any dictionary passed to it. I worked around the issue creating a Word document, defining a text range, changing the LanguageID of this range to the language I want to proof read against, and then inspecting detected spelling errors. Here is the code:
<#Function which helps to pick the language#>
function FindLanguage($language_name){
foreach($element in $Word.Languages){
if($element.Name -eq $language_name){
$element.Name
return $element
}
}
}
$Proofread_text = "The lazie frog jumpss over over the small dog."
$Word = New-Object -COM Word.Application
$Document = $Word.Documents.Add()
$Textrange = $Document.Range(0)
$english = FindLanguage("English (US)")
$Textrange.LanguageID = $english.ID
$Textrange.InsertAfter($Proofread_text)
<#Handle misspelled words here#>
foreach($spell_error in $textrange.SpellingErrors){
Write-Host $spell_error.Text
}
$Document.Close(0)
$Word.Quit()
The output will be:
>>lazie
>>jumpss
>>over
I found it helpful to disable the language autodetection in word before starting the script. Especially when you plan to switch the language.