Azure Blob Storage Image Bulk Optimization
Use PowerShell to quickly optimize all your Azure blob storage PNG and JPEG image files in a single script execution.
Solution Guide
What It Does
This script bulk optimizes the file size of all PNG and JPEG image files in an Azure blob storage container in a single execution. After the script is run, all image blob files in the target container will be processed into an optimal, minimal file size without loss of quality by using standard compression utilities.
Search Engine Optimization (SEO) Value
Azure blob storage can be used to host publicly-accessible images for websites in Azure Web Apps or elsewhere. For example, all images on this page reside in Azure blob storage and have been optimized using this script.
One of the many ranking factors Google, Bing, and other search engines reportedly use for search engine results is website performance, i.e. how fast the page loads for the user. Fast sites are preferred and slow ones penalized (to some degree). One common tool to measure page performance is Google’s PageSpeed Insights tool, which among other things reports whether it believes the images on the page are larger than they should be. It then recommends you “optimize the following images to reduce their size”, but how? There are many tools available on the web, including those leveraged in this script.
After this script is run, PageSpeed will recognize all images as having been optimized.
Before:
After:
How it Works
This script assumes you have a Microsoft Azure subscription which has a blob storage account with a container that holds image files as blobs. When it runs, it connects to Azure using credentials you supply (or those already cached from previous connections) and interactively allows you to select your subscription, storage account, and container. It then downloads each image locally, runs the image optimization tool against it, and uploads the resulting optimized file back to Azure, replacing the original contents.
Upon uploading the optimized image file, the script also sets a custom metadata flag to indicate that the image has been processed. This is the key “optimized” with value “true”. If the script runs again, it will ignore all files it sees have already been optimized, so they are not processed again. This allows the script to be run as many times as needed in an efficient manner, optimizing only new files added since the last run.
Compression Tools
The compression utilities used are jpegtran and OptiPNG, both freely available for download as an executable file. These must exist in the same directory as the script. These are included in the download package from this page or can be downloaded independently.
Optionally, get them here:
Microsoft Azure PowerShell
Ensure the Azure PowerShell module is installed. If not, the script will tell you about that fact in red lettering. It can be downloaded and installed as described here.
Azure Credentials
The script connects to Azure using credentials that have access to the Azure subscription with the images you want to optimize. Therefore, the credentials supplied must be a co-administrator of the subscription.
The account can be a Microsoft account, an Office365 organization account, or an Azure Active Directory account. The prompt is a standard web form that will indicate login problems.
Be aware that cached credentials will be used if present. If you want to make sure to connect with a certain account, run the script with the “-PromptCredentials” switch:
How to Use
To run the script, follow these delightfully easy steps:
-
Download the script and files via the link at the top of the page
-
(Optional) Download the compression tool files mentioned above if you are discerning about the source of executable files on your system
-
Ensure the PowerShell script and EXE files are unblocked
-
Ensure your PowerShell execution policy allows running scripts
-
Right-click the script file and choose Run with PowerShell, or open with PowerShell ISE and run, or open a PowerShell command prompt and run the script as follows:
The script runs and prompts for credentials if none have been previously cached. It then allows you to select the target storage account and container that has the image files you want to optimize.
Taking it Further
This version of the script is interactive, meaning the user is prompted several times. To use this as part of an automated workflow, it could be adapted to remove the prompts and allow specifying the required information via script parameters. If this is something you’re interested in, let us know.
Script
<# .SYNOPSIS Optimizes image file size for all PNG and JPG images in a Microsoft Azure blob storage container by processing them locally and replacing their content with the optimized result. .DESCRIPTION Image files are often needlessly oversized and can be compressed without quality loss using widely-available utilities. Microsoft Azure provides a blob storage service that can be used to host publicly-accessible images for websites or other purposes. For containers with large numbers of suboptimal images or for bulk optimization, this script optimizes all images in a blob storage container with a single action. When run the script prompts for Azure credentials if not already present in the PowerShell session. It then connects and interactively allows the user to select a subscription, storage account, and container where the images reside. It then downloads each file (those blobs with png or jpg/jpeg extensions), optimizes using jpegtran.exe and optipng.exe utilities, and uploads the result to the blob. Upon upload the blob is marked as optimized using custom metadata. This allows the script to be run again without reprocessing optimzed images. The open source jpegtran.exe and optipng.exe executable files must exist in the same directory as the script. In addition, the Azure PowerShell module must be installed. .PARAMETER PromptCredentials By default the script will connect to Azure with any existing credentials that have been used in previous connections. By including this switch, the script will always prompt for credentials. .EXAMPLE To run from a PowerShell prompt: .\Optimize-AzureBlobImages.ps1 .EXAMPLE To run from a PowerShell prompt with credentials prompt: .\Optimize-AzureBlobImages.ps1 -PromptCredentials .INPUTS None. .OUTPUTS No objects returned. .NOTES For more details and implementation guidance, see the associated documentation at https://automys.com #> [CmdletBinding()] Param( [switch]$PromptCredentials = $false, $Command # Placeholder parameter to workaround Windows 7 right-click run bug. Not used. ) Write-Host Hello # Verify Azure module installed/loaded if($null -ne (Get-Module -ListAvailable "Azure")) { Import-Module Azure } else { Write-Host "ERROR: PowerShell module for Microsoft Azure not found. Please install as described at http://azure.microsoft.com/en-us/documentation/articles/powershell-install-configure/" -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } # Verify optimization executable files present if(-not (Test-Path "$PSScriptRoot\optipng.exe")) { Write-Host "Could not find the file optipng.exe in the script file folder [$PSScriptRoot]. Please download from [http://optipng.sourceforge.net/] and copy it there. Exiting script." -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } if(-not (Test-Path "$PSScriptRoot\jpegtran.exe")) { Write-Host "Could not find the file jpegtran.exe in the script file folder [$PSScriptRoot]. Please download from [http://jpegclub.org/jpegtran/] and copy it there. Exiting script." -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } # Prompt for credentials if none are stored or user specified to prompt Write-Host Connecting to Azure... if($PromptCredentials -eq $true -or (Get-AzureAccount).Count -eq 0) { Add-AzureAccount } # Get subscriptions $subscriptions = @() $subscriptions += Get-AzureSubscription if($subscriptions.Count -eq 0) { Write-Host "No subscriptions were found for this account" -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } # If more than one subscription available, show a selection prompt to user if($subscriptions.Count -gt 1) { $choices = @() $i = 0 foreach($subscription in $subscriptions) { $choice = New-Object System.Management.Automation.Host.ChoiceDescription "&$i. $($subscription.SubscriptionName)","Use this subscription" $choices += $choice $i++ } $cancelChoice = New-Object System.Management.Automation.Host.ChoiceDescription "(&Cancel)","Exits the script" $choices += $cancelChoice # Prompt the user select subscription $title = "Select Azure Subscription" $message = "More than one Azure subscription is available. Which one contains the target storage account?" $selectionIndex = $host.ui.PromptForChoice($title, $message, $choices, 0) if($choices[$selectionIndex].Label -eq "(Cancel)") { Write-Host Script cancelled return } $targetSubscription = $subscriptions[$selectionIndex].SubscriptionName } else { $targetSubscription = $subscriptions[0].SubscriptionName Write-Host Defaulting to only subscription found } # Set current subscription Select-AzureSubscription -SubscriptionName $targetSubscription Write-Host "Using subscription: [$targetSubscription]" -ForegroundColor Green # Get available storage accounts $storageAccounts = @() $storageAccounts += Get-AzureStorageAccount -WarningAction SilentlyContinue if($storageAccounts.Count -eq 0) { Write-Host "No storage accounts were found in the subscription [$targetSubscription]. Exiting script." -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } # If more than one storage account available, show a selection prompt to user if($storageAccounts.Count -gt 1) { $choices = @() $i = 0 foreach($storageAccount in $storageAccounts) { $choice = New-Object System.Management.Automation.Host.ChoiceDescription "&$i. $($storageAccount.StorageAccountName)","Use this storage account" $choices += $choice $i++ } $cancelChoice = New-Object System.Management.Automation.Host.ChoiceDescription "(&Cancel)","Exits the script" $choices += $cancelChoice # Prompt the user select subscription $title = "Select storage account" $message = "More than one storage account is available in subscription [$targetSubscription]. Which one has the target storage container?" $selectionIndex = $host.ui.PromptForChoice($title, $message, $choices, 0) if($choices[$selectionIndex].Label -eq "(Cancel)") { Write-Host Script cancelled return } $targetStorageAccount = $storageAccounts[$selectionIndex].StorageAccountName } else { $targetStorageAccount = $storageAccounts[0].StorageAccountName Write-Host Defaulting to only storage account found } # Set current storage account Set-AzureSubscription -SubscriptionName $targetSubscription -CurrentStorageAccountName $targetStorageAccount Write-Host "Using storage account: [$targetStorageAccount]" -ForegroundColor Green # Get available storage containers $storageContainers = @() $storageContainers += Get-AzureStorageContainer if($storageContainers.Count -eq 0) { Write-Host "No containers were found in the storage account [$targetStorageAccount]. Exiting script." -ForegroundColor Red Read-Host "Press [Enter] to exit..." return } # If more than one storage account available, show a selection prompt to user if($storageContainers.Count -gt 1) { $choices = @() $i = 0 foreach($storageContainer in $storageContainers) { $choice = New-Object System.Management.Automation.Host.ChoiceDescription "&$i. $($storageContainer.Name)","Optimize images in this container" $choices += $choice $i++ } $cancelChoice = New-Object System.Management.Automation.Host.ChoiceDescription "(&Cancel)","Exits the script" $choices += $cancelChoice # Prompt the user select subscription $title = "Select storage container" $message = "More than one container exists in the storage account. Which one has the images to optimize?" $selectionIndex = $host.ui.PromptForChoice($title, $message, $choices, 0) if($choices[$selectionIndex].Label -eq "(Cancel)") { Write-Host Script cancelled return } $targetStorageContainer = $storageContainers[$selectionIndex].Name } else { $targetStorageContainer = $storageContainers[0].Name Write-Host Defaulting to only container in the storage account } Write-Host "Selected storage container: [$targetStorageContainer]" -ForegroundColor Green $sourceImages = @() # Get PNG images $sourceImages += Get-AzureStorageBlob -Container $targetStorageContainer -Blob "*png" # Get JPG images $sourceImages += Get-AzureStorageBlob -Container $targetStorageContainer -Blob "*jp*g" $totalImageCount = $sourceImages.Count if($totalImageCount -eq 0) { Write-Host "No PNG or JPG images found in the container. Exiting script." -ForegroundColor Cyan Read-Host "Press [Enter] to exit..." return } # Filter to only those not marked as previously optimized $sourceImages = $sourceImages | where {!$_.ICloudBlob.Metadata.ContainsKey("optimized")} if($sourceImages.Count -eq 0) { Write-Host "All [ $totalImageCount ] images in the container are marked as previously optimized (with 'optimized = true' custom metadata value). Exiting script" -ForegroundColor Cyan Read-Host "Press [Enter] to exit..." return } # Prompt the user for confirmation if($ConfirmPreference -eq "High") { $title = "Confirm Optimization" $message = "Do you want to optimize and overwrite [ $($sourceImages.Count) ] PNG and JPG image blobs in the container [$targetStorageAccount\$targetStorageContainer]?" $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Performs optimization" $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Cancels the script" $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) $result = $host.ui.PromptForChoice($title, $message, $options, 0) switch ($result) { 0 { Write-Host "`nOK, giddy up...`n" } 1 { Write-Host "`nNo worries. Action cancelled." return } } } $tempDirectory = $env:TEMP foreach($image in $sourceImages) { $optimized = $false # Download image blob to file $imageFilePath = "$tempDirectory\$($image.Name)" $image | Get-AzureStorageBlobContent -Destination $tempDirectory -Force | Out-Null $initialSize = Get-ChildItem $imageFilePath | select -ExpandProperty Length Write-Host Processing $image.Name ... if($image.Name -like "*png") { # Optimize with optipng.exe and save output to variable &"$PSScriptRoot\optipng.exe" $imageFilePath 2>&1 | Tee-Object -Variable result | Out-Null if(($result -match "Input file size|already optimized" | Measure | select -ExpandProperty Count) -gt 0) { $optimized = $true } } elseif($image.Name -like "*jp*g") { # Optimize with jpegtran.exe and save output to variable &"$PSScriptRoot\jpegtran.exe" -optimize -verbose $imageFilePath $imageFilePath 2>&1 | Tee-Object -Variable result | Out-Null if(($result -match "End of Image" | Measure | select -ExpandProperty Count) -gt 0) { $optimized = $true } } else { Write-Host "Skipping file with unexpected extension: [$imageFilePath]" -ForegroundColor Yellow } $processedSize = Get-ChildItem $imageFilePath | select -ExpandProperty Length [int]$savingsPercent = ($initialSize - $processedSize) / $initialSize * 100 Write-Host `tSaved ($initialSize - $processedSize) bytes [ $savingsPercent% ] if($optimized -eq $true) { # Upload optimized file to replace blob, retaining all existing metadata and adding a custom "optimized=true" marker to prevent future attempts $metadata = @{"optimized" = "true"} Set-AzureStorageBlobContent -File $imageFilePath -Container $targetStorageContainer -Metadata $metadata -Force | Out-Null } # Remove temp file Remove-Item $imageFilePath } Write-Host `nOptimization complete! -ForegroundColor Green