<#
.SYNOPSIS
WebP Converter V3.0
.DESCRIPTION
Converts images (JPG, PNG, TIFF, HEIC) to WebP format.
Supports single file, folder batch, and drag-and-drop input.
Includes optional image resizing and HEIC intermediate conversion.
.AUTHOR
Mohan KV (Trek Traveller)
.VERSION HISTORY
-------------------------------------------------------------------------
V2.0 (Original)
-------------------------------------------------------------------------
- Basic image-to-WebP conversion (JPG, PNG, TIFF, HEIC)
- Single file and folder batch modes
- Quality slider (0-100)
- cwebp.exe path auto-detection with custom path override
- Simple progress bar with status label
- HEIC support via intermediate TIFF conversion (requires Windows
HEIF Extensions from Microsoft Store)
- Drag and drop support for single file or folder
-------------------------------------------------------------------------
V3.0 | March 14, 2026
-------------------------------------------------------------------------
NEW FEATURES:
- [Async Processing] Replaced blocking DoEvents() loop with PowerShell
Runspace + DispatcherTimer for true background
processing — UI stays fully responsive during
batch conversions
- [Cancel Button] Added "Cancel" button to stop an in-progress
conversion gracefully; shows count of files
processed before cancellation; resets all UI
to Ready state cleanly after dismissing dialog
- [Time Remaining] Live estimated time remaining displayed below
the progress bar, calculated from average time
per file (shows "Calculating..." on first file)
- [Image Resizing] New "Resize Options" panel with configurable
Max Width / Max Height fields; maintains aspect
ratio using the same two-step algorithm as
industry-standard web compressors — this is the
primary reason output can reach ~500 KB from a
12 MB source image
- [Resize Presets] One-click preset buttons: 4K (3840x2160),
FHD (1920x1080), HD (1280x720), Web (1024x768)
- [Multi-file Drop] Drag and drop multiple files at once; auto-
switches to folder mode and queues all dropped
files for conversion
- [Better Validation] Actionable error messages for missing cwebp.exe
(with download link), missing folders/files,
HEIC codec, and invalid resize dimensions
- [Case-insensitive] File browser now shows images with uppercase
extensions (.JPG, .JPEG, .PNG, etc.) correctly
- [Error Summary] Completion dialog lists up to 5 individual file
errors with details instead of just the last one
- [Process Disposal] cwebp Process objects properly disposed via
try/finally to prevent handle/memory leaks
- [Stderr Fix] StandardError read before WaitForExit() to
prevent deadlock on large error output buffers
- [Min Window Size] Window cannot be resized below 650x790 — prevents
UI controls from being clipped or hidden
-------------------------------------------------------------------------
#>
# 1. Load Assemblies
Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, System
# 2. HELPER: Find cwebp
function Find-Cwebp {
# --- USER CUSTOM PATH ---
$customPath = "C:\Users\Administrator\Desktop\WebPConverter\libwebp-1.3.0-windows-x64\libwebp-1.3.0-windows-x64\bin\cwebp.exe"
if (Test-Path $customPath) { return $customPath }
# Fallback search
$scriptPath = $PSScriptRoot
if ($scriptPath -and (Test-Path "$scriptPath\cwebp.exe")) { return "$scriptPath\cwebp.exe" }
try {
$cmd = Get-Command cwebp -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
} catch {}
$searchPaths = @(
"$env:ProgramFiles\libwebp\bin",
"${env:ProgramFiles(x86)}\libwebp\bin",
"$env:USERPROFILE\Downloads"
)
foreach ($path in $searchPaths) {
if (Test-Path "$path\cwebp.exe") { return "$path\cwebp.exe" }
}
return $null
}
# 3. GUI LAYOUT (XAML)
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WebP Converter" Height="790" Width="650"
WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip"
MinWidth="650" MinHeight="790"
Background="#F0F0F0" AllowDrop="True">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Background" Value="#DDDDDD"/>
</Style>
<Style TargetType="GroupBox">
<Setter Property="Margin" Value="10"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="BorderBrush" Value="#AAAAAA"/>
<Setter Property="Background" Value="White"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="24"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Header -->
<TextBlock Grid.Row="0" Text="WebP Image Converter" FontSize="18"
FontWeight="Bold" Margin="15,15,15,5" Foreground="#333333"/>
<TextBlock Grid.Row="0" Text="Drag and drop files/folders here"
FontSize="11" Foreground="#666666" Margin="15,40,15,5"
FontStyle="Italic"/>
<!-- Config -->
<GroupBox Grid.Row="1" Header="Configuration">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="cwebp Path:"/>
<TextBox Name="txtCwebp" Grid.Row="0" Grid.Column="1" Margin="5"/>
<Button Name="btnBrowseCwebp" Grid.Row="0" Grid.Column="2"
Content="..."/>
<Label Grid.Row="1" Grid.Column="0" Content="Quality (0-100):"/>
<DockPanel Grid.Row="1" Grid.Column="1" LastChildFill="True"
Margin="5">
<TextBlock Name="lblQualityPercent" Text="85"
DockPanel.Dock="Right" Width="30"
TextAlignment="Center" VerticalAlignment="Center"
FontWeight="Bold"/>
<Slider Name="sliderQuality" Minimum="0" Maximum="100"
Value="85" TickFrequency="1"
IsSnapToTickEnabled="True"
VerticalAlignment="Center"/>
</DockPanel>
</Grid>
</GroupBox>
<!-- Resize Options -->
<GroupBox Grid.Row="2" Header="Resize Options" Margin="10,0,10,5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<CheckBox Name="chkResize" Grid.Row="0" Grid.ColumnSpan="5"
Content="Resize image (maintains aspect ratio)"
Margin="5,5,5,8" FontWeight="SemiBold"/>
<Label Grid.Row="1" Grid.Column="0" Content="Max Width (px):"/>
<TextBox Name="txtMaxWidth" Grid.Row="1" Grid.Column="1"
Margin="5" Text="1920"/>
<Label Grid.Row="1" Grid.Column="2"
Content="Max Height (px):"/>
<TextBox Name="txtMaxHeight" Grid.Row="1" Grid.Column="3"
Margin="5" Text="1080"/>
<StackPanel Grid.Row="1" Grid.Column="4"
Orientation="Horizontal">
<Button Name="btnPreset4K" Content="4K" Width="40"
Margin="2" ToolTip="3840x2160"/>
<Button Name="btnPresetFHD" Content="FHD" Width="45"
Margin="2" ToolTip="1920x1080"/>
<Button Name="btnPresetHD" Content="HD" Width="40"
Margin="2" ToolTip="1280x720"/>
<Button Name="btnPresetWeb" Content="Web" Width="50"
Margin="2" ToolTip="1024x768"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- Tabs -->
<TabControl Name="tabInput" Grid.Row="3" Margin="10" Height="130">
<TabItem Header=" Folder Mode ">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0"
Content="Source Folder:"/>
<TextBox Name="txtSourceFolder" Grid.Row="0"
Grid.Column="1" Margin="5"/>
<Button Name="btnBrowseFolder" Grid.Row="0"
Grid.Column="2" Content="Browse"/>
<CheckBox Name="chkRecursive" Grid.Row="1"
Grid.Column="1"
Content="Include Subfolders (Recursive)"
Margin="5,10,0,0"/>
</Grid>
</TabItem>
<TabItem Header=" Single File Mode ">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0"
Content="Source File:"/>
<TextBox Name="txtSourceFile" Grid.Row="0"
Grid.Column="1" Margin="5"/>
<Button Name="btnBrowseFile" Grid.Row="0"
Grid.Column="2" Content="Browse"/>
</Grid>
</TabItem>
</TabControl>
<!-- Output -->
<GroupBox Grid.Row="4" Header="Output" Margin="10,5,10,5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="Output Folder:"/>
<TextBox Name="txtOutput" Grid.Column="1" Margin="5"
ToolTip="Empty = Same as source"/>
<Button Name="btnBrowseOutput" Grid.Column="2"
Content="..."/>
</Grid>
</GroupBox>
<!-- Progress -->
<StackPanel Grid.Row="5" Margin="15">
<TextBlock Name="lblStatus" Text="Ready" Margin="0,0,0,5"
TextTrimming="CharacterEllipsis"/>
<ProgressBar Name="progressBar" Height="25" Minimum="0"
Maximum="100" Value="0"/>
<TextBlock Name="lblTimeEstimate" Text="" Margin="0,5,0,0"
FontSize="11" Foreground="#666666"
TextAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="6" Background="#E0E0E0">
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="15,0,0,0" FontSize="11" Foreground="#555555">
Copyrights to Mohan KV (Trek Traveller) - 2026
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
Margin="10">
<Button Name="btnCancel" Content="Cancel" FontWeight="Bold"
Width="100" Height="30" Margin="0,0,10,0"
IsEnabled="False" Background="#FFD700"/>
<Button Name="btnConvert" Content="Start Conversion"
FontWeight="Bold" Width="150" Height="30"/>
</StackPanel>
</Grid>
</Grid>
</Window>
"@
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)
# 4. CONTROLS
function Get-Ctrl { param($Name) return $window.FindName($Name) }
$txtCwebp = Get-Ctrl "txtCwebp"
$btnBrowseCwebp = Get-Ctrl "btnBrowseCwebp"
$sliderQuality = Get-Ctrl "sliderQuality"
$lblQuality = Get-Ctrl "lblQualityPercent"
$tabInput = Get-Ctrl "tabInput"
$txtSourceFolder = Get-Ctrl "txtSourceFolder"
$btnBrowseFolder = Get-Ctrl "btnBrowseFolder"
$chkRecursive = Get-Ctrl "chkRecursive"
$txtSourceFile = Get-Ctrl "txtSourceFile"
$btnBrowseFile = Get-Ctrl "btnBrowseFile"
$txtOutput = Get-Ctrl "txtOutput"
$btnBrowseOutput = Get-Ctrl "btnBrowseOutput"
$progressBar = Get-Ctrl "progressBar"
$lblStatus = Get-Ctrl "lblStatus"
$lblTimeEstimate = Get-Ctrl "lblTimeEstimate"
$btnConvert = Get-Ctrl "btnConvert"
$btnCancel = Get-Ctrl "btnCancel"
$chkResize = Get-Ctrl "chkResize"
$txtMaxWidth = Get-Ctrl "txtMaxWidth"
$txtMaxHeight = Get-Ctrl "txtMaxHeight"
$btnPreset4K = Get-Ctrl "btnPreset4K"
$btnPresetFHD = Get-Ctrl "btnPresetFHD"
$btnPresetHD = Get-Ctrl "btnPresetHD"
$btnPresetWeb = Get-Ctrl "btnPresetWeb"
# Set Path
$cwebpLoc = Find-Cwebp
if ($cwebpLoc) { $txtCwebp.Text = $cwebpLoc }
# ADDED .heic to allowed extensions
$validExts = @(".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".heic")
# Cancellation flag and runspace tracking
$script:cancelRequested = $false
$script:activeRunspace = $null
$script:syncHash = [hashtable]::Synchronized(@{
Progress = 0
Status = "Ready"
TimeEstimate = ""
IsComplete = $false
Results = $null
})
# 5. EVENTS
$sliderQuality.Add_ValueChanged({
$lblQuality.Text = [Math]::Round($sliderQuality.Value).ToString()
})
$btnPreset4K.Add_Click({
$txtMaxWidth.Text = "3840"; $txtMaxHeight.Text = "2160"
$chkResize.IsChecked = $true
})
$btnPresetFHD.Add_Click({
$txtMaxWidth.Text = "1920"; $txtMaxHeight.Text = "1080"
$chkResize.IsChecked = $true
})
$btnPresetHD.Add_Click({
$txtMaxWidth.Text = "1280"; $txtMaxHeight.Text = "720"
$chkResize.IsChecked = $true
})
$btnPresetWeb.Add_Click({
$txtMaxWidth.Text = "1024"; $txtMaxHeight.Text = "768"
$chkResize.IsChecked = $true
})
$btnBrowseCwebp.Add_Click({
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "cwebp.exe|cwebp.exe|All Files|*.*"
if ($ofd.ShowDialog() -eq "OK") { $txtCwebp.Text = $ofd.FileName }
})
$btnBrowseFolder.Add_Click({
$fbd = New-Object System.Windows.Forms.FolderBrowserDialog
if ($fbd.ShowDialog() -eq "OK") {
$txtSourceFolder.Text = $fbd.SelectedPath
}
})
$btnBrowseFile.Add_Click({
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$allExts = $validExts | ForEach-Object {
"*$_"; "*$($_.ToUpper())"
}
$filter = "Images|" + ($allExts -join ";")
$ofd.Filter = $filter
if ($ofd.ShowDialog() -eq "OK") {
$txtSourceFile.Text = $ofd.FileName
}
})
$btnBrowseOutput.Add_Click({
$fbd = New-Object System.Windows.Forms.FolderBrowserDialog
if ($fbd.ShowDialog() -eq "OK") {
$txtOutput.Text = $fbd.SelectedPath
}
})
$window.Add_DragOver({
param($s,$e)
$e.Effects = [System.Windows.DragDropEffects]::Copy
$e.Handled = $true
})
$window.Add_Drop({
param($s,$e)
if ($e.Data.GetDataPresent(
[System.Windows.DataFormats]::FileDrop)) {
$files = $e.Data.GetData(
[System.Windows.DataFormats]::FileDrop)
if ($files.Count -gt 0) {
if (Test-Path $files[0] -PathType Container) {
$tabInput.SelectedIndex = 0
$txtSourceFolder.Text = $files[0]
$lblStatus.Text = "Folder loaded."
} else {
if ($files.Count -eq 1) {
$tabInput.SelectedIndex = 1
$txtSourceFile.Text = $files[0]
$lblStatus.Text = "File loaded."
} else {
$tabInput.SelectedIndex = 0
$firstDir = [System.IO.Path]::GetDirectoryName(
$files[0])
$txtSourceFolder.Text = $firstDir
$script:droppedFiles = $files
$lblStatus.Text = "$($files.Count) files loaded."
}
}
}
}
})
# 6. CANCEL HANDLER
$btnCancel.Add_Click({
$script:cancelRequested = $true
$btnCancel.IsEnabled = $false
$lblStatus.Text = "Cancellation requested..."
})
# 7. CONVERSION LOGIC (ASYNC WITH RUNSPACE)
$btnConvert.Add_Click({
$exe = $txtCwebp.Text
# Validation
if ([string]::IsNullOrWhiteSpace($exe) -or
-not (Test-Path $exe -PathType Leaf)) {
[System.Windows.MessageBox]::Show(
"cwebp.exe not found or invalid path.`n`nPlease:`n" +
"1. Download libwebp from " +
"https://developers.google.com/speed/webp/download`n" +
"2. Extract the archive`n" +
"3. Browse to cwebp.exe location",
"Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Error)
return
}
$quality = [Math]::Round($sliderQuality.Value)
if ($quality -lt 0 -or $quality -gt 100) {
[System.Windows.MessageBox]::Show(
"Quality must be between 0 and 100.",
"Validation Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
return
}
$doResize = [bool]$chkResize.IsChecked
$maxWidth = 1920
$maxHeight = 1080
if ($doResize) {
if (-not [int]::TryParse($txtMaxWidth.Text,
[ref]$maxWidth) -or $maxWidth -le 0) {
[System.Windows.MessageBox]::Show(
"Max Width must be a positive number.",
"Validation Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
return
}
if (-not [int]::TryParse($txtMaxHeight.Text,
[ref]$maxHeight) -or $maxHeight -le 0) {
[System.Windows.MessageBox]::Show(
"Max Height must be a positive number.",
"Validation Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
return
}
}
$filesToProcess = @()
if ($tabInput.SelectedIndex -eq 0) {
if ($script:droppedFiles) {
$filesToProcess = $script:droppedFiles |
ForEach-Object { Get-Item $_ } |
Where-Object {
$validExts -contains $_.Extension.ToLower()
}
$script:droppedFiles = $null
} else {
if (-not (Test-Path $txtSourceFolder.Text)) {
[System.Windows.MessageBox]::Show(
"Folder not found.`n`nPlease:`n" +
"1. Click Browse to select a folder, or`n" +
"2. Drag and drop a folder into this window",
"Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
return
}
if ($chkRecursive.IsChecked) {
$filesToProcess = Get-ChildItem `
-Path $txtSourceFolder.Text -Recurse -File |
Where-Object {
$validExts -contains $_.Extension.ToLower()
}
} else {
$filesToProcess = Get-ChildItem `
-Path $txtSourceFolder.Text -File |
Where-Object {
$validExts -contains $_.Extension.ToLower()
}
}
}
} else {
if (-not (Test-Path $txtSourceFile.Text)) {
[System.Windows.MessageBox]::Show(
"File not found.`n`nPlease:`n" +
"1. Click Browse to select a file, or`n" +
"2. Drag and drop image files into this window",
"Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
return
}
$filesToProcess = @(Get-Item $txtSourceFile.Text)
}
if ($filesToProcess.Count -eq 0) {
[System.Windows.MessageBox]::Show(
"No valid image files found!`n`n" +
"Supported formats: JPG, PNG, BMP, TIFF, HEIC",
"No Files",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Information)
return
}
# Setup
$script:cancelRequested = $false
$btnConvert.IsEnabled = $false
$btnCancel.IsEnabled = $true
$progressBar.Value = 0
$lblTimeEstimate.Text = ""
$outDirUser = $txtOutput.Text
$script:syncHash.Progress = 0
$script:syncHash.Status = "Starting..."
$script:syncHash.TimeEstimate = ""
$script:syncHash.IsComplete = $false
$script:syncHash.Results = $null
$script:syncHash.DialogShown = $false
# Create Runspace for Async Processing
$runspace = [runspacefactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$script:activeRunspace = $runspace
$runspace.SessionStateProxy.SetVariable(
"files", $filesToProcess)
$runspace.SessionStateProxy.SetVariable("exe", $exe)
$runspace.SessionStateProxy.SetVariable(
"quality", $quality)
$runspace.SessionStateProxy.SetVariable(
"outDirUser", $outDirUser)
$runspace.SessionStateProxy.SetVariable(
"validExts", $validExts)
$runspace.SessionStateProxy.SetVariable(
"syncHash", $script:syncHash)
$runspace.SessionStateProxy.SetVariable(
"cancelFlag", ([ref]$script:cancelRequested))
$runspace.SessionStateProxy.SetVariable(
"doResize", $doResize)
$runspace.SessionStateProxy.SetVariable(
"maxWidth", $maxWidth)
$runspace.SessionStateProxy.SetVariable(
"maxHeight", $maxHeight)
$powershell = [powershell]::Create()
$powershell.Runspace = $runspace
[void]$powershell.AddScript({
param($files, $exe, $quality, $outDirUser,
$syncHash, $cancelFlagRef,
$doResize, $maxWidth, $maxHeight)
$total = $files.Count
$success = 0
$errors = 0
$errorList = @()
$count = 0
$startTime = Get-Date
foreach ($file in $files) {
if ($cancelFlagRef.Value) {
$syncHash.Status = "Cancelling..."
break
}
$count++
$percent = [int](($count / $total) * 100)
$elapsed = (Get-Date) - $startTime
if ($count -gt 1) {
$avgTimePerFile = $elapsed.TotalSeconds / ($count - 1)
$remaining = $total - $count
$estimatedSeconds = [int](
$avgTimePerFile * $remaining)
$timeText = if ($estimatedSeconds -gt 60) {
"$([int]($estimatedSeconds / 60))m " +
"$($estimatedSeconds % 60)s remaining"
} else {
"${estimatedSeconds}s remaining"
}
} else {
$timeText = "Calculating..."
}
$syncHash.Progress = $percent
$syncHash.Status = "Converting $count of " +
"${total}: $($file.Name)"
$syncHash.TimeEstimate = $timeText
if ([string]::IsNullOrWhiteSpace($outDirUser)) {
$targetFolder = $file.DirectoryName
} else {
$targetFolder = $outDirUser
}
if (!(Test-Path $targetFolder)) {
New-Item -ItemType Directory `
-Path $targetFolder -Force | Out-Null
}
$targetPath = Join-Path $targetFolder (
$file.BaseName + ".webp")
# --- HEIC HANDLING ---
$inputPath = $file.FullName
$isHeic = ($file.Extension.ToLower() -eq ".heic")
$tempTiff = ""
$tempResized = ""
if ($isHeic) {
$syncHash.Status = "Processing HEIC: " +
"$($file.Name)..."
$tempTiff = Join-Path $targetFolder (
$file.BaseName + "_temp_" +
[Guid]::NewGuid().ToString() + ".tiff")
try {
Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Image]::FromFile(
$inputPath)
$img.Save($tempTiff,
[System.Drawing.Imaging.ImageFormat]::Tiff)
$img.Dispose()
$inputPath = $tempTiff
} catch {
$errors++
$errorList += "$($file.Name): HEIC conversion" +
" failed. Install 'HEIF Image Extensions'" +
" from Microsoft Store. Error: $_"
continue
}
}
# --- RESIZE LOGIC ---
if ($doResize) {
try {
Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Image]::FromFile(
$inputPath)
$origWidth = $img.Width
$origHeight = $img.Height
$newWidth = $origWidth
$newHeight = $origHeight
if ($newWidth -gt $maxWidth) {
$newHeight = [int](
($newHeight * $maxWidth) / $newWidth)
$newWidth = $maxWidth
}
if ($newHeight -gt $maxHeight) {
$newWidth = [int](
($newWidth * $maxHeight) / $newHeight)
$newHeight = $maxHeight
}
if ($newWidth -ne $origWidth -or
$newHeight -ne $origHeight) {
$syncHash.Status = "Resizing " +
"$($file.Name) to " +
"${newWidth}x${newHeight}..."
$tempResized = Join-Path $targetFolder (
$file.BaseName + "_resized_" +
[Guid]::NewGuid().ToString() + ".png")
$bmp = New-Object System.Drawing.Bitmap(
$newWidth, $newHeight)
$gfx = [System.Drawing.Graphics]::FromImage(
$bmp)
$gfx.InterpolationMode =
[System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$gfx.SmoothingMode =
[System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$gfx.PixelOffsetMode =
[System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$gfx.DrawImage($img, 0, 0,
$newWidth, $newHeight)
$bmp.Save($tempResized,
[System.Drawing.Imaging.ImageFormat]::Png)
$gfx.Dispose()
$bmp.Dispose()
$inputPath = $tempResized
}
$img.Dispose()
} catch {
$errors++
$errorList += "$($file.Name): " +
"Resize failed - $_"
continue
}
}
# Run cwebp with proper disposal
$p = $null
try {
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = $exe
$p.StartInfo.Arguments =
"-q $quality `"$inputPath`" " +
"-o `"$targetPath`""
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.CreateNoWindow = $true
[void]$p.Start()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($p.ExitCode -eq 0) {
$success++
} else {
$errors++
$errorList += "$($file.Name): " +
"Conversion failed. $stderr"
}
} catch {
$errors++
$errorList += "$($file.Name): " +
"Process error - $_"
} finally {
if ($p) { $p.Dispose() }
}
# Cleanup Temp Files
if ($isHeic -and $tempTiff -and
(Test-Path $tempTiff)) {
Remove-Item $tempTiff -Force `
-ErrorAction SilentlyContinue
}
if ($tempResized -and
(Test-Path $tempResized)) {
Remove-Item $tempResized -Force `
-ErrorAction SilentlyContinue
}
}
# Return final results via sync hash
$syncHash.IsComplete = $true
$syncHash.Results = @{
Success = $success
Errors = $errors
ErrorList = $errorList
Total = $total
Cancelled = $cancelFlagRef.Value
}
}).AddArgument($filesToProcess
).AddArgument($exe
).AddArgument([int]$quality
).AddArgument($outDirUser
).AddArgument($script:syncHash
).AddArgument([ref]$script:cancelRequested
).AddArgument($doResize
).AddArgument([int]$maxWidth
).AddArgument([int]$maxHeight)
$asyncResult = $powershell.BeginInvoke()
# Timer to update UI
$timer = New-Object `
System.Windows.Threading.DispatcherTimer
$timer.Interval = [TimeSpan]::FromMilliseconds(100)
$timer.Add_Tick({
if (-not $script:syncHash.IsComplete) {
$progressBar.Value = $script:syncHash.Progress
$lblStatus.Text = $script:syncHash.Status
$lblTimeEstimate.Text = $script:syncHash.TimeEstimate
}
if ($script:syncHash.IsComplete -and
-not $script:syncHash.DialogShown) {
$timer.Stop()
$script:syncHash.DialogShown = $true
$progressBar.Value = 0
$lblTimeEstimate.Text = ""
$lblStatus.Text = "Ready"
$btnConvert.IsEnabled = $true
$btnCancel.IsEnabled = $false
try {
$finalResult = $script:syncHash.Results
if ($finalResult) {
if ($finalResult.Cancelled) {
[System.Windows.MessageBox]::Show(
"Conversion cancelled by user.`n`n" +
"Processed: " +
"$($finalResult.Success + $finalResult.Errors)" +
" of $($finalResult.Total)",
"Cancelled",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Information)
} elseif ($finalResult.Errors -gt 0) {
$progressBar.Value = 100
$lblStatus.Text = "Finished with errors."
$errorSummary = $finalResult.ErrorList |
Select-Object -First 5 |
ForEach-Object { "• $_" }
$errorMsg = "Finished with issues.`n" +
"Success: $($finalResult.Success)`n" +
"Failed: $($finalResult.Errors)`n`n" +
"First errors:`n" +
($errorSummary -join "`n")
if ($finalResult.ErrorList.Count -gt 5) {
$errorMsg += "`n... and " +
"$($finalResult.ErrorList.Count - 5)" +
" more errors"
}
[System.Windows.MessageBox]::Show(
$errorMsg, "Warning",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Warning)
} else {
$progressBar.Value = 100
$lblStatus.Text = "Done."
[System.Windows.MessageBox]::Show(
"Success! Converted " +
"$($finalResult.Success) files to " +
"WebP format.",
"Done",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Information)
}
}
} catch {
[System.Windows.MessageBox]::Show(
"An unexpected error occurred:`n$_",
"Error",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Error)
} finally {
$powershell.Dispose()
$runspace.Close()
$runspace.Dispose()
$script:activeRunspace = $null
}
}
})
$timer.Start()
})
$window.ShowDialog() | Out-Null
Copy Code