ozo-windows-installer-customizer.ps1

#Requires -Modules Dism,OZOLogger -Version 5.1 -RunAsAdministrator

<#PSScriptInfo
    .VERSION 0.1.0
    .GUID 798c9379-3b13-41c0-a2d9-04c7957dff49
    .AUTHOR Andy Lievertz <alievertz@onezeroone.dev>
    .COMPANYNAME One Zero One
    .COPYRIGHT This script is released under the terms of the GNU Public License ("GPL") version 2.0.
    .TAGS
    .LICENSEURI https://github.com/onezeroone-dev/OZO-Windows-Installer-Customizer/blob/main/LICENSE
    .PROJECTURI https://github.com/onezeroone-dev/OZO-Windows-Installer-Customizer
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES Dism,OZOLogger
    .REQUIREDSCRIPTS
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES https://github.com/onezeroone-dev/OZO-Windows-Installer-Customizer/blob/main/CHANGELOG.md
    .PRIVATEDATA
#>


<#
    .SYNOPSIS
    See description.
    .DESCRIPTION
    Customizes the Windows installer ISO based on a JSON configuration file containing parameters for OS, version, edition, and features. It enables automation with an Answer File, can include custom media (wallpapers, logos, etc.), and can remove undesired AppX packages
    .PARAMETER Configuration
    The path to the JSON configuration file. Defaults to ozo-windows-installer-customizer.json in the same directory as this script.
    .PARAMETER Nocleanup
    Do not clean up the temporary file assets. Mostly used for testing and debugging.
    .EXAMPLE
    ozo-windows-installer-customizer -Configuration "C:\Imaging\Configuration\ozo-windows-installer-customizer.json"
    .LINK
    https://github.com/onezeroone-dev/OZO-Windows-Installer-Customizer/blob/main/README.md
    .NOTES
    Run this script as Administrator.
#>


[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$false,HelpMessage="The path to the JSON configuration file")][String]$Configuration = (Join-Path -Path $PSScriptRoot -ChildPath "ozo-windows-installer-customizer.json"),
    [Parameter(HelpMessage="Do not clean up the temporary file assets")][Switch]$NoCleanup
)

# Classes
Class OWICMain {
    # PROPERTIES: Booleans, Strings, Longs
    [Boolean] $Validates = $true
    [Long]    $tempFree  = $null
    [String]  $jsonPath  = $null
    # PROPERTIES: PSCustomObjects
    [PSCustomObject] $Json      = $null
    [PSCustomObject] $ozoLogger = (New-OZOLogger)
    # PROPERTIES: Lists
    [System.Collections.Generic.List[String]] $Editions = @(
        "Home",
        "Home N",
        "Home Single Language",
        "Education",
        "Education N",
        "Pro",
        "Pro N",
        "Pro Education",
        "Pro Education N",
        "Pro for Workstations",
        "Pro N for Workstations",
        "Enterprise",
        "Enterprise N",
        "Enterprise Evaluation",
        "Enterprise N Evaluation"
        "Standard Evaluation",
        "Standard Evaluation (Desktop Experience)",
        "Datacenter Evaluation",
        "Datacenter Evaluation (Desktop Experience)"
    )
    [System.Collections.Generic.List[PSCustomObject]] $Jobs = @()
    # METHODS
    # Constructor method
    OWICMain($Configuration,$NoCleanup) {
        # Set Properties
        $this.jsonPath  = $Configuration
        # Declare ourselves to the world
        $this.ozoLogger.Write("Starting process.","Information")
        # Call ValidateEnvironment to set Validates
        If (($this.ValidateConfiguration() -And $this.ValidateEnvironment()) -eq $true) {
            # Iterate through the enabled jobs in the JSON configuration
            ForEach($jobJson in ($this.Json.Jobs | Where-Object {$_.Enabled -eq $true})) {
                # Add OscdimgPath, tempDir, and tempFree to the job object
                Add-Member -InputObject $jobJson -MemberType NoteProperty -Name "OscdimgPath" -Value $this.Json.Paths.OscdimgPath
                Add-Member -InputObject $jobJson -MemberType NoteProperty -Name "tempDir" -Value $this.Json.Paths.tempDir
                Add-Member -InputObject $jobJson -MemberType NoteProperty -Name "noCleanup" -Value $NoCleanup
                Add-Member -InputObject $jobJson -MemberType NoteProperty -Name "tempFree" -Value $this.tempFree
                # Add this job to the jobs list
                $this.Jobs.Add(([OWICJob]::new($jobJson)))
            }
            # Iterate through the valid jobs
            ForEach ($Job in ($this.Jobs | Where-Object {$_.Validates -eq $true})) {
                # Call the CustomizeISO method
                $Job.CustomizeISO()
            }
        } Else {
            # Configuration and environment do not validate
            $this.Validates = $false
        }
        # Call the report method
        $this.Report()
        # Finish
        $this.ozoLogger.Write("Process complete.","Information")
    }
    # JSON validation method
    Hidden [Boolean] ValidateConfiguration() {
        # Control variable
        [Boolean] $Return = $true
        # Determine if the JSON file exists
        If ((Test-Path $this.jsonPath) -eq $true) {
            # File exists; try to convert it from JSON
            Try {
                $this.Json = (Get-Content $this.jsonPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
                # Success
            } Catch {
                # Failure
                $this.ozoLogger.Write("Invalid JSON.","Error")
                $Return = $false
            }
        } Else {
            # File does not exist
            $this.ozoLogger.Write("Configuration file does not exists or is not accessible.","Error")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Paths validation method
    Hidden [Boolean] ValidateEnvironment() {
        # Control variable
        [Boolean] $Return = $true
        # Local variables
        [String] $tempDrive = $null
        # Scrutinize TempDir; determine if the path contains spaces
        If ($this.Json.Paths.TempDir -Match " ") {
            # Found spaces
            $this.ozoLogger.Write("Path to temporary directory cannot contain spaces. This is a limitation of the Microsoft oscdimg.exe command.","Error")
            $Return = $false
        }
        # Determine if TempDir exists
        Try {
            Test-Path -Path $this.Json.Paths.TempDir -ErrorAction Stop
            # Success; get statistics
            $tempDrive = (Get-Item $this.Json.Paths.TempDir).PSDrive.Name
            $this.tempFree = (Get-Volume -DriveLetter $tempDrive).SizeRemaining
            # Determine if TempDir drive is fixed
            If ((Get-Volume -DriveLetter $tempDrive).DriveType -ne "Fixed") {
                # Disk is not fixed
                $this.ozoLogger.Write("Temporary directory is not on a fixed drive.","Error")
                $Return = $false
            }
        } Catch {
            # Path does not exist
            $this.ozoLogger.Write("Temporary directory does not exist.","Error")
            $Return = $false
        }
        # Determine if oscdimg.exe is missing
        Try {
            Test-Path -Path $this.Json.Paths.OscdimgPath -ErrorAction Stop
            # Success
        } Catch {
            # Failure
            $this.ozoLogger.Write("Did not find oscdimg.exe.","Error")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Report class
    [Void] Report() {
        # Determine if any jobs were processed
        If ($this.Jobs.Count -gt 0) {
            # At least one job was processed; determine if there were any successes
            If (($this.Jobs | Where-Object {$_.Success -eq $true}).Count -gt 0) {
                # At least one job was successful; report names
                $this.ozoLogger.Write(("Successfully processed the following jobs:`r`n" + (($this.Jobs | Where-Object {$_.Success -eq $true}).Json.Name -Join("`r`n"))),"Information")
                # Iterate on the success jobs
                ForEach ($successJob in ($this.Jobs | Where-Object {$_.Success -eq $true})) {
                    $this.ozoLogger.writeOutput = $false
                    $this.ozoLogger.Write(("The " + $successJob.Json.Name + " job succeeded with the following messages:`r`n" + ($successJob.Messages -Join("`r`n"))),"Information")
                    $this.ozoLogger.writeOutput = $true
                }
            }
            # Determine if there were any failures
            If (($this.Jobs | Where-Object {$_.Success -eq $false}).Count -gt 0) {
                # At least one job failed; report names
                $this.ozoLogger.Write(("The following jobs failed:`r`n" + (($this.Jobs | Where-Object {$_.Success -eq $false}).Json.Name -Join("`r`n"))),"Warning")
                # Iterate
                ForEach ($failureJob in ($this.Jobs | Where-Object {$_.Success -eq $false})) {
                    $this.ozoLogger.writeOutput = $false
                    $this.ozoLogger.Write(("The " + $failureJob.Json.Name + " job failed with the following messages:`r`n" + ($failureJob.Messages -Join("`r`n"))),"Warning")
                    $this.ozoLogger.writeOutput = $true
                }
            }
            # Report re: additional messages.
            $this.ozoLogger.Write("Please see the One Zero One Windows Event Provider Log for additional detail. If you have not installed the One Zero One Windows Event Provider, messages can be found in the Microsoft Windows PowerShell Event Log Provider under event ID 4100.","Information")
        } Else {
            # No jobs were processed
            $this.ozoLogger.Write("Processed zero jobs.","Warning")
        }
    }
}

Class OWICJob {
    # PROPERTES: Boolean, Long, String, PSCUstomObject
    [Boolean] $Validates     = $true
    [Boolean] $Success       = $true
    [String]  $dvdDir        = $null
    [String]  $iconFile      = $null
    [String]  $jobTempDir    = $null
    [String]  $logoFile      = $null
    [String]  $mountDir      = $null
    [String]  $mountDrive    = $null
    [String]  $OscdimgPath   = $null
    [String]  $targetISOPath = $null
    [String]  $wallpaperFile = $null
    [String]  $wimDir        = $null
    # PROPERTIES: PSCUstomObject
    [PSCustomObject] $Json   = $null
    # PROPERTIES: List
    [System.Collections.Generic.List[String]] $Messages = @()
    # METHODS
    # Constructor method
    OWICJob($Json) {
        # Set properties
        $this.Json       = $Json
        $this.jobTempDir = (Join-Path -Path $this.Json.tempDir -ChildPath ((New-Guid).Guid + "-ozo-windows-installer-customizer"))
        # Declare ourselves to the world
        $this.Messages.Add("Processing job.")
        # Call ValidateJob to set Validates
        If ($this.ValidateJob() -eq $true) {
            # Job validated
            $this.Messages.Add("Job validates.")
            $this.Validates = $true
        } Else {
            # Job did not validate
            $this.Messages.Add("Job does not validate.")
            $this.Validates = $false
            $this.Success   = $false
        }
    }
    # Validate job method
    Hidden [Boolean] ValidateJob() {
        # Control variable
        [Boolean] $Return = $true
        # Local variables
        [Xml] $answerXml = $null
        # Determine if Name is set
        If ([String]::IsNullOrEmpty($this.Json.Name) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing Name.")
            $Return = $false
        }
        # Determine if OSName is set
        If ([String]::IsNullOrEmpty($this.Json.OSName) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing OSName.")
            $Return = $false
        }
        # Determine if Version is set
        If ([String]::IsNullOrEmpty($this.Json.Version) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing Version.")
            $Return = $false
        }
        # Determine if Edition is set
        If ([String]::IsNullOrEmpty($this.Json.Edition) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing Edition.")
            $Return = $false
        }
        # Determine if Feature is set
        If ([String]::IsNullOrEmpty($this.Json.Feature) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing Feature.")
            $Return = $false
        }
        # Determine if Build is set
        If ([String]::IsNullOrEmpty($this.Json.Build) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing Build.")
            $Return = $false
        }
        # Determine if answerPath is set
        If ([String]::IsNullOrEmpty($this.Json.Files.answerPath) -eq $true) {
            # Not set; warn
            $this.Messages.Add("Job configuration is missing answerPath.")
            $Return = $false
        } Else {
            # Set; Determine that file exists
            If ((Test-Path -Path $this.Json.Files.answerPath) -eq $true){
                # File exists; try to determine if XML is valid
                Try {
                    $answerXml = [Xml](Get-Content -Path $this.Json.Files.answerPath -ErrorAction Stop)
                    # Success (valid XML); try to get the logo file name
                    Try {
                        $this.logoFile = (Split-Path -Path (($answerXml.unattend.settings | Where-Object {$_.pass -eq "specialize"}).component | Where-Object {$_.name -eq "Microsoft-Windows-Shell-Setup"}).OEMInformation.Logo -Leaf -ErrorAction Stop)
                        # Success
                    } Catch {
                        # Failure
                        $this.Messages.Add("Unable to read logo path from Autounattend XML.")
                    }
                    # Try to get the icon file name
                    Try {
                        $this.iconFile = (Split-Path -Path (($answerXml.unattend.settings | Where-Object {$_.pass -eq "oobeSystem"}).component | Where-Object {$_.name -eq "Microsoft-Windows-Shell-Setup"}).Themes.BrandIcon -Leaf -ErrorAction Stop)
                        # Success
                    } Catch {
                        # Failure
                        $this.Messages.Add("Unable to read icon path from Autounattend XML.")
                    }
                    # Try to get the wallpaper file name
                    Try {
                        $this.wallpaperFile = (Split-Path -Path (($answerXml.unattend.settings | Where-Object {$_.pass -eq "oobeSystem"}).component | Where-Object {$_.name -eq "Microsoft-Windows-Shell-Setup"}).Themes.DesktopBackground -Leaf -ErrorAction SilentlyContinue)
                        # Success
                    } Catch {
                        # Failure
                        $this.Messages.Add("Unable to read wallpaper path from Autounattend XML.")
                    }
                } Catch {
                    # Failure (invalid XML)
                    $this.Messages.Add("Answer file contains invalid XML.")
                    $Return = $false
                }
            } Else {
                # File does not exist; error
                $this.Messages.Add("Answer file specified but file not found.")
                $Return = $false
            }
        }
        # Determine if sourceISOPath is set
        If ([String]::IsNullOrEmpty($this.Json.Files.sourceISOPath) -eq $true) {
            # Not set; error
            $this.Messages.Add("Job configuration is missing sourceISOPath.")
        } Else {
            # Set; determine that path does not contain spaces
            If ($this.Json.Files.sourceISOPath -Match " ") {
                # Path contains spaces - oscdimg.exe cannot handle spaces
                $this.Messages.Add("The sourceISOPath contains spaces. Due to a limitation with oscdimg.exe, this path cannot contain spaces.")
                $Return = $false
            } 
            # Determine that file exists
            If ((Test-Path -Path $this.Json.Files.sourceISOPath) -eq $true) {
                # File exists; parse to set targetISOPath
                $this.targetISOPath = (Join-Path -Path (Split-Path -Path $this.Json.Files.sourceISOPath -Parent) -ChildPath ("OZO-" + $this.Json.OSName + "-" + $this.Json.Version + "-" + $this.Json.Edition + "-" + $this.Json.Feature + "-" + $this.Json.Build + ".iso"))
                # Determine if target ISO already exists
                If ((Test-Path -Path $this.targetISOPath) -eq $true) {
                    # Target ISO exists; error
                    $this.Messages.Add("Target ISO exists; skipping.")
                    $Return = $false
                }
                # File exists; determine if the disk has enough space for this job
                If ($this.Json.tempFree -lt ((Get-Item -Path $this.Json.Files.sourceISOPath).Length * 4)) {
                    # Disk does not have enough space
                    $this.Messages.Add("Drive does not have enough free space; requires size of ISO x 4.")
                    $Return = $false
                }
            } Else {
                # File does not exist; error
                $this.Messages.Add("Source ISO specified but file not found.")
                $Return = $false
            }
        }
        # Determine if logoPath is set
        If ([String]::IsNullOrEmpty($this.Json.Files.logoPath) -eq $false) {
            # Set; determine that file exists
            If((Test-Path -Path $this.Json.Files.logoPath) -eq $false) {
                # File does not exist; warn
                $this.Messages.Add("Logo path specified but file not found.")
                $Return = $false
            }
        }
        # Determine if iconPath is set
        If ([String]::IsNullOrEmpty($this.Json.Files.iconPath) -eq $false) {
            # Set; determine that file exists
            If((Test-Path -Path $this.Json.Files.iconPath) -eq $false) {
                # File does not exist; warn
                $this.Messages.Add("Icon path specified but file not found.")
                $Return = $false
            }
        }
        # Determine if wallpaperPath is set
        If ([String]::IsNullOrEmpty($this.Json.Files.wallpaperPath) -eq $false) {
            # Set; determine that file exists
            If((Test-Path -Path $this.Json.Files.wallpaperPath) -eq $false) {
                # File does not exist; warn
                $this.Messages.Add("Wallpaper path specified but file not found.")
                $Return = $false
            }
        }
        # Determine if any drivers directories are set
        If ($this.Json.Drivers.Count -gt 0) {
            # At least one drivers directory is set; iterate
            ForEach ($Driver in $this.Json.Drivers) {
                # Determine if the path is not found
                If ((Test-Path -Path $Driver) -eq $false) {
                    # Path is not found
                    $this.Messages.Add(("Missing drivers directory " + $Driver + "."))
                    $Return = $false
                }
            }
        } Else {
            # No drivers directories are set
            $this.Messages.Add("No drivers directories specified.")
        }
        # Determine if any AppXProvisionedPackages are set
        If($this.Json.removeAppxProvisionedPackages.Count -eq 0) {
            # No AppXPackages set
            $this.Messages.Add("No AppxPackages specified.")
        }
        # Return
        return $Return
    }
    # Customize ISO method
    [Void] CustomizeISO() {
        # Call all methods in order to set Success
        $this.Success = (
            $this.CreateJobTempDirs() -And
            $this.MountISO() -And
            $this.CopyISO() -And
            $this.MoveWIM() -And
            $this.ExportIndex() -And
            $this.MountWIM() -And
            $this.CopyMediaAssets() -And
            $this.AddDrivers() -And
            $this.RemoveAppxPackages() -and
            $this.DismountWIM() -And
            $this.CopyAnswerFile() -And
            $this.WriteISO()
        )
        # Call Cleanup to clean up temporary file assets
        $this.Cleanup()
    }
    # Create job temporary directories method
    Hidden [Boolean] CreateJobTempDirs() {
        # Control variable
        [Boolean] $Return = $true
        # Attempt to create jobTempDir
        Try {
            New-Item -ItemType Directory -Path $this.jobTempDir -ErrorAction Stop
            # Success; set paths
            $this.dvdDir   = (Join-Path -Path $this.jobTempDir -ChildPath "DVD")
            $this.wimDir   = (Join-Path -Path $this.jobTempDir -ChildPath "WIM")
            $this.mountDir = (Join-Path -Path $this.jobTempDir -ChildPath "Mount")
            # Try to create paths
            Try {
                New-Item -ItemType Directory -Path $this.dvdDir -Force -ErrorAction Stop
                New-Item -ItemType Directory -Path $this.wimDir -Force -ErrorAction Stop
                New-Item -ItemType Directory -Path $this.mountDir -Force -ErrorAction Stop
                # Success
            } Catch {
                # Failure
                $this.Messages.Add(("Unable to create one or more subdirectories of " + $this.jobTempDir))
                $Return = $false
            }
        } Catch {
            # Failure
            $this.Messages.Add("Failed to create temporary job directory")
            $Return = $false
        }

        # Return
        return $Return
    }
    # Mount ISO method
    Hidden [Boolean] MountISO() {
        # Control variable
        [Boolean] $Return = $true
        Try {
            Mount-DiskImage -ImagePath $this.Json.Files.sourceISOPath -ErrorAction Stop
            # Success; get the drive letter
            $this.mountDrive = (Get-DiskImage $this.Json.Files.sourceISOPath -ErrorAction Stop | Get-Volume -ErrorAction Stop).DriveLetter
        } Catch {
            # Failure
            $this.Messages.Add("Failed to mount source ISO")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Copy ISO method
    Hidden [Boolean] CopyISO() {
        # Control variable
        [Boolean] $Return = $true
        Try {
            Copy-Item -Path ($this.mountDrive + ":\*") -Recurse -Destination ($this.dvdDir + "\") -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to copy ISO contents to the DVD directory")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Move WIM method
    Hidden [Boolean] MoveWIM() {
        # Control variable
        [Boolean] $Return = $true
        Try {
            Move-Item -Path (Join-Path -Path $this.dvdDir -ChildPath "sources\install.wim") -Destination ($this.wimDir + "\") -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to move the WIM")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Export index method
    Hidden [Boolean] ExportIndex() {
        # Control variable
        [Boolean] $Return    = $true
        [String]  $IndexName = ($this.Json.OSName + " " + $this.Json.Version + " " + $this.Json.Edition)
        Try {
            Export-WindowsImage -SourceName $IndexName -SourceImagePath (Join-Path -Path $this.wimDir -ChildPath "install.wim") -DestinationImagePath (Join-Path -Path $this.dvdDir -ChildPath "sources\install.wim") -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add(("Failed to export the " + $IndexName + " index"))
            $Return = $false
        }
        # Return
        return $Return
    }
    # Mount WIM method
    Hidden [Boolean] MountWIM() {
        # Control variable
        [Boolean] $Return = $true
        Try {
            Mount-WindowsImage -Path $this.mountDir -ImagePath (Join-Path -Path $this.dvdDir -ChildPath "sources\install.wim") -Index 1 -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to mount WIM")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Copy media assets method
    Hidden [Boolean] CopyMediaAssets() {
        # Control variable
        [Boolean] $Return = $true
        # Determine if any of the media files are set
        If ([String]::IsNullOrEmpty($this.logoFile) -eq $false -Or [String]::IsNullOrEmpty($this.iconFile) -eq $false -Or [String]::IsNullOrEmpty($this.wallpaperFile) -eq $false) {
        # Try to create the OEM directory
            Try {
                New-Item -ItemType Directory -Path (Join-Path -Path $this.mountDir -ChildPath "Windows\System32\OEM") -ErrorAction Stop
                # Success; determine if logoPath was set in the XML
                If ([String]::IsNullOrEmpty($this.logoFile) -eq $false) {
                    # logoPath was set in the XML; determine if the logoPath is set in the job configuration and the file exists
                    If ([String]::IsNullOrEmpty($this.Json.Files.logoPath) -eq $false -And (Test-Path -Path $this.Json.Files.logoPath) -eq $true) {
                        # logoPath is set in the job configuration and the file exists; try to copy
                        Try {
                            Copy-Item -Path $this.Json.Files.logoPath -Destination (Join-Path -Path $this.mountDir -ChildPath (Join-Path -Path "Windows\System32\OEM" -ChildPath $this.logoFile)) -ErrorAction Stop
                            #Success
                        } Catch {
                            # Failure
                            $this.Messages.Add("Failed to copy logo file.")
                            $Return = $false
                        }
                    } Else {
                        $this.Messages.Add("Logo is specified in the Autounattend XML but (a) not specified in the job configuration or (b) specified in the job configuration but the file is missing or not accessible.")
                        $Return = $false
                    }
                } Else {
                    $this.Messages.Add("No logo path found in the Autounattend XML.")
                }
                # Determine if iconPath is set
                If ([String]::IsNullOrEmpty($this.iconFile) -eq $false) {
                    # iconPath was set in the XML; determine if the iconPath is set in the job configuration and the file exists
                    If ([String]::IsNullOrEmpty($this.Json.Files.iconPath) -eq $false -And (Test-Path -Path $this.Json.Files.iconPath) -eq $true) {
                        # logoPath is set in the job configuration and the file exists; try to copy
                        Try {
                            Copy-Item -Path $this.Json.Files.iconPath -Destination (Join-Path -Path $this.mountDir -ChildPath (Join-Path -Path "Windows\System32\OEM" -ChildPath $this.iconFile)) -ErrorAction Stop
                            #Success
                        } Catch {
                            # Failure
                            $this.Messages.Add("Failed to copy icon file.")
                            $Return = $false
                        }
                    } Else {
                        $this.Messages.Add("Icon is specified in the Autounattend XML but (a) not specified in the job configuration or (b) specified in the job configuration but the file is missing or not accessible.")
                        $Return = $false
                    }
                } Else {
                    $this.Messages.Add("No icon path found in the Autounattend XML.")
                }
                # Determine if wallpaperPath is set
                If ([String]::IsNullOrEmpty($this.wallpaperFile) -eq $false) {
                    # wallpaperPath was set in the XML; determine if the wallpaperPath is set in the job configuration and the file exists
                    If ([String]::IsNullOrEmpty($this.Json.Files.wallpaperPath) -eq $false -And (Test-Path -Path $this.Json.Files.wallpaperPath) -eq $true) {
                        # logoPath is set in the job configuration and the file exists; try to copy
                        Try {
                            Copy-Item -Path $this.Json.Files.wallpaperPath -Destination (Join-Path -Path $this.mountDir -ChildPath (Join-Path -Path "Windows\System32\OEM" -ChildPath $this.wallpaperFile)) -ErrorAction Stop
                            #Success
                        } Catch {
                            # Failure
                            $this.Messages.Add("Failed to copy logo file.")
                            $Return = $false
                        }
                    } Else {
                        $this.Messages.Add("Wallpaper is specified in the Autounattend XML but (a) not specified in the job configuration or (b) specified in the job configuration but the file is missing or not accessible.")
                        $Return = $false
                    }
                } Else {
                    $this.Messages.Add("No wallpaper path found in the Autounattend XML.")
                }
            } Catch {
                $this.Messages.Add("Unable to create OEM directory for media assets.")
                $Return = $false
            }
        } Else {
            # No media files are set
            $this.Messages.Add("No media files specified.")
        }
        # Return
        return $Return
    }
    # Add drivers method
    Hidden [Boolean] AddDrivers() {
        # Control variable
        [Boolean] $Return = $true
        # Iterate through the Models
        ForEach ($Driver in $this.Drivers) {
            # Try to add drivers
            Try {
                Add-WindowsDriver -Path $this.mountDir -Driver $Driver -Recurse -ErrorAction Stop
                #Success
            } Catch {
                # Failure
                $this.Messages.Add(("Failed to add " + $Driver + " to image."))
                $Return = $false
            }
        }
        # Return
        return $Return
    }
    # RemoveAppxPackages method
    Hidden [Boolean] RemoveAppxPackages() {
        # Control variable
        [Boolean] $Return = $true
        # Determine if there are any packages to be removed
        If ($this.Json.removeAppxProvisionedPackages.Count -gt 0) {
            # One or more packages have been identified for removal; iterate through them
            ForEach ($AppxPackage in (Get-AppxProvisionedPackage -Path $this.mountDir)) {
                # Determine if this package appears in the image
                If ($this.removeAppxProvisionedPackages -Contains $AppxPackage.DisplayName) {
                    # Package appears in the image; try to remove
                    Try {
                        Remove-AppXProvisionedPackage -Path $this.mountDir -PackageName $AppxPackage.PackageName -ErrorAction Stop
                        # Success
                    } Catch {
                        # Failure
                        $this.Messages.Add(("Unable to remove the " + $AppxPackage.DisplayName + " package from the image."))
                    }
                }
            }
        } Else {
            $this.Messages.Add("No AppxPackages to remove.")
        }
        # Return
        return $Return
    }
    # Unmount WIM method
    Hidden [Boolean] DismountWIM() {
        # Control variable
        [Boolean] $Return = $true
        # Try to dismount the WIM
        Try {
            Dismount-WindowsImage -Path $this.mountDir -Save -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to dismount the WIM.")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Copy answer file method
    Hidden [Boolean] CopyAnswerFile() {
        # Control variable
        [Boolean] $Return = $true
        Try {
            Copy-Item -Path $this.Json.Files.answerPath -Destination ($this.dvdDir + "\Autounattend.xml") -ErrorAction Stop
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to copy Answer file.")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Write ISO method
    Hidden [Boolean] WriteISO() {
        # Control variable
        [Boolean] $Return = $true
        # Try to write the ISO
        Try {
            Start-Process -NoNewWindow -Wait -FilePath $this.Json.OscdimgPath -ArgumentList ('-u2 -udfver102 -t -l' + [System.IO.Path]::GetFileNameWithoutExtension($this.targetISOPath) + ' -b' + (Join-Path -Path $this.dvdDir -ChildPath "efi\microsoft\boot\efisys.bin") + ' ' + ($this.dvdDir + "\") + ' ' + $this.targetISOPath)
            #Success
        } Catch {
            # Failure
            $this.Messages.Add("Failed to write ISO.")
            $Return = $false
        }
        # Return
        return $Return
    }
    # Cleanup method
    Hidden [Void] Cleanup() {
        # Determine if there are any mounted images
        If ((Get-WindowsImage -Mounted) -Contains $this.mountDir) {
            # There are mounted images and one of them contains mountDir; try to unmount
            Try {
                Dismount-WindowsImage -Path $this.MountDir -Discard -ErrorAction Stop
                # Success;
            } Catch {
                $this.Messages.Add("Unable to unmount image.")
            }
        }
        # Determine if the ISO is still mounted
        If ($null -ne (Get-DiskImage $this.Json.Files.sourceISOPath | Get-Volume).DriveLetter) {
            # ISO is still mounted; try to dismount
            Try {
                Dismount-DiskImage -ImagePath $this.Json.Files.sourceISOPath -ErrorAction Stop
                # Success
            } Catch {
                # Failure
                $this.Messages.Add("Unable to unmount ISO.")
            }
        }
        # Determine if operator did not request NoCleanup
        If ($this.Json.noCleanup -eq $false) {
            # Operator did not request NoCleanup; try to remove the jobTempDir
            Try {
                Remove-Item -Recurse -Force -Path $this.jobTempDir -ErrorAction Stop
            } Catch {
                $this.Messages.Add(("Unable to remove job temporary directory " + $this.jobTempDir + ". Please delete manually."))
            }
        }
    }
}

# MAIN
[OWICMain]::new($Configuration,$NoCleanup) | Out-Null

# SIG # Begin signature block
# MIIfcQYJKoZIhvcNAQcCoIIfYjCCH14CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB17kiY3PcmRw9f
# S0WKdkQblG32YGKn6TN1agojR00mmaCCDPgwggZyMIIEWqADAgECAghkM1HTxzif
# CDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjA2MjQyMDQ0MzBaFw0zMTA2MjQyMDQ0MzBaMHgxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENv
# cnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBD
# QSBSU0EgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCfgxNzqrDG
# bSHL24t6h3TQcdyOl3Ka5LuINLTdgAPGL0WkdJq/Hg9Q6p5tePOf+lEmqT2d0bKU
# Vz77OYkbkStW72fL5gvjDjmMxjX0jD3dJekBrBdCfVgWQNz51ShEHZVkMGE6ZPKX
# 13NMfXsjAm3zdetVPW+qLcSvvnSsXf5qtvzqXHnpD0OctVIFD+8+sbGP0EmtpuNC
# GVQ/8y8Ooct8/hP5IznaJRy4PgBKOm8yMDdkHseudQfYVdIYyQ6KvKNc8HwKp4WB
# wg6vj5lc02AlvINaaRwlE81y9eucgJvcLGfE3ckJmNVz68Qho+Uyjj4vUpjGYDdk
# jLJvSlRyGMwnh/rNdaJjIUy1PWT9K6abVa8mTGC0uVz+q0O9rdATZlAfC9KJpv/X
# gAbxwxECMzNhF/dWH44vO2jnFfF3VkopngPawismYTJboFblSSmNNqf1x1KiVgMg
# Lzh4gL32Bq5BNMuURb2bx4kYHwu6/6muakCZE93vUN8BuvIE1tAx3zQ4XldbyDge
# VtSsSKbt//m4wTvtwiS+RGCnd83VPZhZtEPqqmB9zcLlL/Hr9dQg1Zc0bl0EawUR
# 0tOSjAknRO1PNTFGfnQZBWLsiePqI3CY5NEv1IoTGEaTZeVYc9NMPSd6Ij/D+KNV
# t/nmh4LsRR7Fbjp8sU65q2j3m2PVkUG8qQIDAQABo4H7MIH4MA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU3QQJB6L1en1SUxKSle44gCUNplkwMAYIKwYBBQUH
# AQEEJDAiMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTARBgNVHSAE
# CjAIMAYGBFUdIAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwOwYDVR0fBDQwMjAwoC6g
# LIYqaHR0cDovL2NybHMuc3NsLmNvbS9zc2wuY29tLXJzYS1Sb290Q0EuY3JsMB0G
# A1UdDgQWBBRUwv4QlQCTzWr158DX2bJLuI8M4zAOBgNVHQ8BAf8EBAMCAYYwDQYJ
# KoZIhvcNAQELBQADggIBAPUPJodwr5miyvXWyfCNZj05gtOII9iCv49UhCe204MH
# 154niU2EjlTRIO5gQ9tXQjzHsJX2vszqoz2OTwbGK1mGf+tzG8rlQCbgPW/M9r1x
# xs19DiBAOdYF0q+UCL9/wlG3K7V7gyHwY9rlnOFpLnUdTsthHvWlM98CnRXZ7WmT
# V7pGRS6AvGW+5xI+3kf/kJwQrfZWsqTU+tb8LryXIbN2g9KR+gZQ0bGAKID+260P
# Z+34fdzZcFt6umi1s0pmF4/n8OdX3Wn+vF7h1YyfE7uVmhX7eSuF1W0+Z0duGwdc
# +1RFDxYRLhHDsLy1bhwzV5Qe/kI0Ro4xUE7bM1eV+jjk5hLbq1guRbfZIsr0WkdJ
# LCjoT4xCPGRo6eZDrBmRqccTgl/8cQo3t51Qezxd96JSgjXktefTCm9r/o35pNfV
# HUvnfWII+NnXrJlJ27WEQRQu9i5gl1NLmv7xiHp0up516eDap8nMLDt7TAp4z5T3
# NmC2gzyKVMtODWgqlBF1JhTqIDfM63kXdlV4cW3iSTgzN9vkbFnHI2LmvM4uVEv9
# XgMqyN0eS3FE0HU+MWJliymm7STheh2ENH+kF3y0rH0/NVjLw78a3Z9UVm1F5VPz
# iIorMaPKPlDRADTsJwjDZ8Zc6Gi/zy4WZbg8Zv87spWrmo2dzJTw7XhQf+xkR6Od
# MIIGfjCCBGagAwIBAgIQZ2iSsNbwOsjnLExSAX6F6DANBgkqhkiG9w0BAQsFADB4
# MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
# ETAPBgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmlu
# ZyBJbnRlcm1lZGlhdGUgQ0EgUlNBIFIxMB4XDTI0MTExNjEwMzUyOFoXDTI1MTEx
# NjEwMzUyOFowZTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8wDQYD
# VQQHDAZEZW52ZXIxGDAWBgNVBAoMD0FuZHJldyBMaWV2ZXJ0ejEYMBYGA1UEAwwP
# QW5kcmV3IExpZXZlcnR6MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA
# vIBAQzK0aahepOrPmvCEqfd6dMZC4GvV7kflKwrn4QPJGfqhFmUtadP1e3ange8O
# QZ3/w7UjOTAUNUHfhjbSgUBlKjbS6EWQKZuRFzI3SNkMJkcjTX4uS2P4QsnwM+SW
# IE5me3CTssdjtgue+Iiy53TMgW8JpoxiULVxmm3bhCRUAgxWeT6tzjytR1UyGcMc
# cm/YE6TOgsCHiZoo4X4HJD9iHDrNldArq04Jl6FsADxEswttKyfqpIRJLoAysVl1
# f8CEDBwhszJrEXBnAlWViJFfNY+dKP4jhf7lLqSvPCuADqP2jvM0Ym5I8qDGMz9j
# XPSMLF58MFB4vM4viS7nLRFJ8S1Q98vQvB8W4kk0WPuiZbZTHsROzohE1VSbLnIY
# ag5dDOWI8L6yutAsfdZFYFmSTKcMSiOj5VbK4LhAJUL2G8vPwpTGFgr+cEp0p62F
# P0WXK+/cRfGqodI5S+bg+9rQTD9zf829DwraSRAt5P5zrQk4WPst3JW/vIKNx7cV
# AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFFTC/hCVAJPN
# avXnwNfZsku4jwzjMHoGCCsGAQUFBwEBBG4wbDBIBggrBgEFBQcwAoY8aHR0cDov
# L2NlcnQuc3NsLmNvbS9TU0xjb20tU3ViQ0EtQ29kZVNpZ25pbmctUlNBLTQwOTYt
# UjEuY2VyMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTBRBgNVHSAE
# SjBIMAgGBmeBDAEEATA8BgwrBgEEAYKpMAEDAwEwLDAqBggrBgEFBQcCARYeaHR0
# cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMD
# ME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMY29tLVN1
# YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNybDAdBgNVHQ4EFgQUSj8HrSK7
# f/j+Dz31jJFhOF7rJUMwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IC
# AQBf4lcc6FUJ1W/opNz8yjS9qLUy9cQt0s35BhasB5QoTbDaW4jv9xnFGhQVg6n+
# jhL0i94Vsywd/MRBb8lYGpuBZnS/7LHuRZu7qUuud+IMDyRHIyBK6koN5bfyA5VY
# c7bFbNpbe1s1hMWke8di4qgMLZKDfyG/RtA0swf5t4UgQLPP0h+koZ8X8V5+P0V0
# 1HsdXyXd+ojo38EoZyCKfQL2aAwMPwzZfCbmI5SRXNOc6K8oqXzQcendhlKSfVBo
# Zgpi+1updqbD4jmJfYdK5AYPxJ3YH6td6ETtr8owL+bmX8lQjlXPOwVnC11rVlNB
# VjqtaJRUClLtiNiYSTKVfjdmGVJ4+sNov0dWhHc0A9o5NX/05VVYTlImuJpnG5Og
# o7w6kWRdsgE8gM58jWf7XfI6aQS0Np/z2B+ZBj0K93khEHBX7cvvORa92LCHiVeP
# km+zEAMXgxIPs/e8cmcc/o3CORgzEwxlH9Z3UOWCuXSHD3P2RPNDAY+WPdjSHm9f
# JFlGq+f9iKyedxYa/NNjNag/5EbZ+Z2NldtSMNeFdsejGJ/TJHF1PyJd4aXx9J1i
# B/IZBOoJYyh9xpQ3ljZUKE/4otPi7INpuDFwgWiUHZZJVvrGTWwxH1Yhf8P+VpFf
# aNqsBuvklUcUDs3RNE0f1qlgFfcnAepFF+RiBRqmsj29fjGCEc8wghHLAgEBMIGM
# MHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3Rv
# bjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWdu
# aW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjECEGdokrDW8DrI5yxMUgF+hegwDQYJ
# YIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYK
# KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG
# 9w0BCQQxIgQgs3mQ55T6Vgr8SxXFactBQIBClyO/LN2wttO/jRQyWkswDQYJKoZI
# hvcNAQEBBQAEggGAMvGSbPYVy1oPFWvJqnrhoLBo9/GypkTt9k9CPxqG4zPEUAS7
# 8hvaz+/vI5Wsev8PuQRYeDcT2aDKq3LfbtTtyPmhzNXZqgBYgwTGL1XgLCI4n5nm
# oPQD2KizyzuGuNHwp+sipD4DhKqJQ58Ym4ZiRXkN4RZ1jw/ldyn9PBwPxfGKcZjp
# H6VgGbC4NvAesX5wuSNDeN1t5lMeW617GjVwy1Gmn3+g49OVJH0GWYX61vaRSf1V
# mZtdNeTfcI5PknL4kRvl4yZzXJQInpOFPsdwxrVMkxBf1C8hjAEGwNWzBmKgGglH
# zkD0cC5dXFLntP42TUvVYmki97Oahof+1B5LvNn49CUChpjucn/7ZMN7edomgmng
# q5eJefNYZnUD2SgaTw12XytxC3kBs5ZZzwYVqVEPftJDVa4zPvChSjAdCOt7nlnQ
# TdyseBRt9zRw1wuIdcgiQSiE1+UUJC5pFEIBm/5TAQAD5eEXFs6pUUPnkpymy+Z0
# NGpvpPpcyDtWjLRooYIPFTCCDxEGCisGAQQBgjcDAwExgg8BMIIO/QYJKoZIhvcN
# AQcCoIIO7jCCDuoCAQMxDTALBglghkgBZQMEAgEwdwYLKoZIhvcNAQkQAQSgaARm
# MGQCAQEGDCsGAQQBgqkwAQMGATAxMA0GCWCGSAFlAwQCAQUABCCKud7k5tkwSFsI
# 8DHpQkZIJTD97fDxNab5PJQX/pMkDAIIMisRpj4+79YYDzIwMjUwNDA2MDIwMjM2
# WjADAgEBoIIMADCCBPwwggLkoAMCAQICEFparOgaNW60YoaNV33gPccwDQYJKoZI
# hvcNAQELBQAwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQH
# DAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNvbSBU
# aW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjEwHhcNMjQwMjE5MTYxODE5WhcN
# MzQwMjE2MTYxODE4WjBuMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO
# BgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMSowKAYDVQQDDCFTU0wu
# Y29tIFRpbWVzdGFtcGluZyBVbml0IDIwMjQgRTEwWTATBgcqhkjOPQIBBggqhkjO
# PQMBBwNCAASnYXL1MOl6xIMUlgVC49zonduUbdkyb0piy2i8t3JlQEwA74cjK8g9
# mRC8GH1cAAVMIr8M2HdZpVgkV1LXBLB8o4IBWjCCAVYwHwYDVR0jBBgwFoAUDJ0Q
# JY6apxuZh0PPCH7hvYGQ9M8wUQYIKwYBBQUHAQEERTBDMEEGCCsGAQUFBzAChjVo
# dHRwOi8vY2VydC5zc2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIx
# LmNlcjBRBgNVHSAESjBIMDwGDCsGAQQBgqkwAQMGATAsMCoGCCsGAQUFBwIBFh5o
# dHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkwCAYGZ4EMAQQCMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEYGA1UdHwQ/MD0wO6A5oDeGNWh0dHA6Ly9jcmxzLnNz
# bC5jb20vU1NMLmNvbS10aW1lU3RhbXBpbmctSS1SU0EtUjEuY3JsMB0GA1UdDgQW
# BBRQTySs77U+YxMjCZIm7Lo6luRdIjAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcN
# AQELBQADggIBAJigjwMAkbyrxGRBf0Ih4r+rbCB57lTuwViC6nH2fZSciMogpqSz
# rSeVZ2eIb5vhj9rT7jqWXZn02Fncs4YTrA1QyxJW36yjC4jl5/bsFCaWuXzGXt2Y
# 6Ifp//A3Z0sNTMWTTBobmceM3sqnovdX9ToRFP+29r5yQnPcgRTI2PvrVSqLxY9E
# yk9/0cviM3W29YBl080ENblRcu3Y8RsfzRtVT/2snuDocRxvRYmd0TPaMgIj2xII
# 651QnPp1hiq9xU0AyovLzbsi5wlR5Ip4i/i8+x+HwYJNety5cYtdWJ7uQP6YaZtW
# /jNoHp76qNftq/IlSx6xEYBRjFBxHSq2fzhUQ5oBawk2OsZ2j0wOf7q7AqjCt6t/
# +fbmWjrAWYWZGj/RLjltqdFPBpIKqdhjVIxaGgzVhaE/xHKBg4k4DfFZkBYJ9BWu
# P93Tm+paWBDwXI7Fg3alGsboErWPWlvwMAmpeJUjeKLZY26JPLt9ZWceTVWuIyuj
# erqb5IMmeqLJm5iFq/Qy4YPGyPiolw5w1k9OeO4ErmS2FKvk1ejvw4SWR+S1VyWn
# ktY442WaoStxBCCVWZdMWFeB+EpL8uoQNq1MhSt/sIUjUudkyZLIbMVQjj7b6gPX
# nD6mS8FgWiCAhuM1a/hgA+6o1sJWizHdmcpYDhyNzorf9KVRE6iR7rcmMIIG/DCC
# BOSgAwIBAgIQbVIYcIfoI02FYADQgI+TVjANBgkqhkiG9w0BAQsFADB8MQswCQYD
# VQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAWBgNV
# BAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENlcnRp
# ZmljYXRpb24gQXV0aG9yaXR5IFJTQTAeFw0xOTExMTMxODUwMDVaFw0zNDExMTIx
# ODUwMDVaMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwH
# SG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20gVGlt
# ZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEArlEQE9L5PCCgIIXeyVAcZMnh/cXpNP8KfzFI6HJaxV6oYf3x
# h/dRXPu35tDBwhOwPsJjoqgY/Tg6yQGBqt65t94wpx0rAgTVgEGMqGri6vCI6rEt
# SZVy9vagzTDHcGfFDc0Eu71mTAyeNCUhjaYTBkyANqp9m6IRrYEXOKdd/eREsqVD
# mhryd7dBTS9wbipm+mHLTHEFBdrKqKDM3fPYdBOro3bwQ6OmcDZ1qMY+2Jn1o0l4
# N9wORrmPcpuEGTOThFYKPHm8/wfoMocgizTYYeDG/+MbwkwjFZjWKwb4hoHT2WK8
# pvGW/OE0Apkrl9CZSy2ulitWjuqpcCEm2/W1RofOunpCm5Qv10T9tIALtQo73GHI
# lIDU6xhYPH/ACYEDzgnNfwgnWiUmMISaUnYXijp0IBEoDZmGT4RTguiCmjAFF5OV
# NbY03BQoBb7wK17SuGswFlDjtWN33ZXSAS+i45My1AmCTZBV6obAVXDzLgdJ1A1r
# yyXz4prLYyfJReEuhAsVp5VouzhJVcE57dRrUanmPcnb7xi57VPhXnCuw26hw1Hd
# +ulK3jJEgbc3rwHPWqqGT541TI7xaldaWDo85k4lR2bQHPNGwHxXuSy3yczyOg57
# TcqqG6cE3r0KR6jwzfaqjTvN695GsPAPY/h2YksNgF+XBnUD9JBtL4c34AcCAwEA
# AaOCAYEwggF9MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAU3QQJB6L1
# en1SUxKSle44gCUNplkwgYMGCCsGAQUFBwEBBHcwdTBRBggrBgEFBQcwAoZFaHR0
# cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkvU1NMY29tUm9vdENlcnRpZmljYXRp
# b25BdXRob3JpdHlSU0EuY3J0MCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3Ns
# LmNvbTA/BgNVHSAEODA2MDQGBFUdIAAwLDAqBggrBgEFBQcCARYeaHR0cHM6Ly93
# d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMIMDsGA1Ud
# HwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmxzLnNzbC5jb20vc3NsLmNvbS1yc2EtUm9v
# dENBLmNybDAdBgNVHQ4EFgQUDJ0QJY6apxuZh0PPCH7hvYGQ9M8wDgYDVR0PAQH/
# BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQCSGXUNplpCzxkH2fL8lPrAm/AV6USW
# Wi9xM91Q5RN7mZN3D8T7cm1Xy7qmnItFukgdtiUzLbQokDJyFTrF1pyLgGw/2hU3
# FJEywSN8crPsBGo812lyWFgAg0uOwUYw7WJQ1teICycX/Fug0KB94xwxhsvJBiRT
# pQyhu/2Kyu1Bnx7QQBA1XupcmfhbQrK5O3Q/yIi//kN0OkhQEiS0NlyPPYoRboHW
# C++wogzV6yNjBbKUBrMFxABqR7mkA0x1Kfy3Ud08qyLC5Z86C7JFBrMBfyhfPpKV
# lIiiTQuKz1rTa8ZW12ERoHRHcfEjI1EwwpZXXK5J5RcW6h7FZq/cZE9kLRZhvnRK
# tb+X7CCtLx2h61ozDJmifYvuKhiUg9LLWH0Or9D3XU+xKRsRnfOuwHWuhWch8G7k
# EmnTG9CtD9Dgtq+68KgVHtAWjKk2ui1s1iLYAYxnDm13jMZm0KpRM9mLQHBK5Gb4
# dFgAQwxOFPBslf99hXWgLyYE33vTIi9p0gYqGHv4OZh1ElgGsvyKdUUJkAr5hfbD
# X6pYScJI8v9VNYm1JEyFAV9x4MpskL6kE2Sy8rOqS9rQnVnIyPWLi8N9K4GZvPit
# /Oy+8nFL6q5kN2SZbox5d69YYFe+rN1sDD4CpNWwBBTI/q0V4pkgvhL99IV2Xasj
# HZf4peSrHdL4RjGCAlcwggJTAgEBMIGHMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI
# DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAt
# BgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxAhBa
# WqzoGjVutGKGjVd94D3HMAsGCWCGSAFlAwQCAaCCAWEwGgYJKoZIhvcNAQkDMQ0G
# CyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA0MDYwMjAyMzZaMCgGCSqG
# SIb3DQEJNDEbMBkwCwYJYIZIAWUDBAIBoQoGCCqGSM49BAMCMC8GCSqGSIb3DQEJ
# BDEiBCC8gbAaavd8JQlxjGk1pz3MYnz2NWYoy1I1kFWi9x67LzCByQYLKoZIhvcN
# AQkQAi8xgbkwgbYwgbMwgbAEIJ1xf43CN2Wqzl5KsOH1ddeaF9Qc7tj9r+8D/T29
# iUfnMIGLMHekdTBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNV
# BAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29t
# IFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQWlqs6Bo1brRiho1XfeA9
# xzAKBggqhkjOPQQDAgRGMEQCICQeKn9Cltjzyldw+dnygM/8SZ6CQoOMy0Bnhtuu
# OR1iAiBtqJoJE8jtn64ewlN3Ly5bgHoR2x7qjdbvp4ZKNRdfVg==
# SIG # End signature block