Did you ever run Google PageSpeed Insights on your website and get a violation for not optimizing your images? I did, so I figured out how to fix it in a pretty painless manner. This issue comes up from time to time, so it’s important to know how to resolve it before it’s too late. No one wants to manually fix 500 images in production via downloading, optimizing, and re-attaching (looking at you, Ben). The solution I want to show you today involves optimizing images in Sitecore with PowerShell and TinyPNG.
Introducing TinyPNG for optimizing images
TinyPNG (https://tinypng.com/) is a site I visit pretty frequently in my day-to-day activities as a Sitecore Developer. Any time I upload an image to the Sitecore CMS, I try to make sure it’s optimized. That’s what TinyPNG does, and it does it well. Fortunately for me, I have a team to help me assemble content, so it’s inevitable that lots of media will be uploaded by a variety of people, some of which may not be optimized for web.
According to TinyPNG, they use “smart lossy compression techniques to reduce the file size of your PNG files. By selectively decreasing the number of colors in the image, fewer bytes are required to store the data. The effect is nearly invisible but it makes a very large difference in file size!”
Typically a lossy compression will result in tremendous file size reduction at the cost of minor visual clarity at very high resolutions. I honestly have never really noticed a visual difference in image quality when using TinyPNG, so I think their lossy algorithm must be pretty good.
Also worth noting: TinyPNG works on .jpg files as well.
Kraken.io: Another excellent solution
If you want a lossless compression (e.g., zero reduction in visual clarity), Kraken.io offers that ability.
I remember doing a proof-of-concept integration between Kraken.io and the mediaUpload pipeline in the past. It worked out beautifully but I never ended up forking the money over for a professional Kraken subscription.
Regardless of which optimization vendor you choose, you can integrate with them easily from PowerShell.
Let’s automate them with PowerShell
Sitecore PowerShell Extensions is my new favorite toy. I can’t believe I went so long without really using it. If you’re not using this module, you should be. There’s excellent documentation and lots of activity in the #module-spe channel of the Sitecore Slack Community. After all, it’s the secret sauce to the image optimization technique I’m about to share with you!
First of all, let’s build out where we’ll install our script:
We’ll create a PowerShell script object under /sitecore/system/Modules/PowerShell/Script Library/My-Tenant/Content Editor/Context Menu/Media/Optimize Image. The “Show if rules are met…” field basically allows the script to be executed on these template types:
- /sitecore/templates/System/Media/Unversioned/Image
- /sitecore/templates/System/Media/Unversioned/Jpeg
- /sitecore/templates/System/Media/Versioned/Image
- /sitecore/templates/System/Media/Versioned/Jpeg
You may want to tweak that a bit depending on your intentions (e.g., allow on any media folder type, to apply this same technique to all descendant items recursively).
And now, some code
Here’s the TinyPNG flavor of this script. Add this to the Script body field of your PowerShell object in Sitecore. Keep in mind that I’m still new to PowerShell, so I apologize for any awkward code. 🙂
# Take a media item and shoot it to TinyPNG's API. Return a URL indicating where the resulting optimized image is function TinifyImage($mediaItem) { $apiKey = "TODO REPLACE WITH API KEY" # generate an API key: https://tinypng.com/developers $apiAuthorization = "api:$apiKey" $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($apiAuthorization)) $basicAuthValue = "Basic $encodedCreds" $Headers = @{ Authorization = $basicAuthValue } $blobField = $mediaItem.InnerItem.Fields["blob"] $blobStream = $blobField.GetBlobStream() Try { $request = Invoke-RestMethod -Method Post -Uri "https://api.tinify.com/shrink" -Headers $Headers -Body $blobStream if ($request.output.url) { return $request.output.url } else { Write-Error "TinyPNG request failed" Write-Host ($request | Format-List | Out-String) } } Catch { Write-Error "TinyPNG request failed" Break } } # Sets the $mediaItem to the image at $url function SetMedia($mediaItem, $url, $extension) { # download file to temporary location $tempFolder = "$SitecoreDataFolder\temp" Test-Path $tempFolder if ((Test-Path $tempFolder) -eq 0) { New-Item -ItemType Directory -Force -Path $tempFolder } # instead of temp.png, you may want to generate a unique name here to avoid collisions $filePath = "$tempFolder\temp.$extension" Invoke-WebRequest -Uri $url -OutFile $filePath # set the media stream to be the file system file $stream = New-Object -TypeName System.IO.FileStream -ArgumentList $filePath, "Open", "Read" [Sitecore.Resources.Media.Media] $media = [Sitecore.Resources.Media.MediaManager]::GetMedia($mediaItem); $media.SetStream($stream, $extension); $stream.Close(); # delete temporary file Remove-Item $filePath } # Execute against current item $location = get-location $scItem = Get-Item $location $mediaItem = New-Object "Sitecore.Data.Items.MediaItem" $scItem $extension = $mediaItem.Extension $url = TinifyImage($mediaItem) if ($location) { Write-Host "Tiny PNG optimized url: $url" SetMedia $mediaItem $url $extension } else { Write-Error "Tiny PNG failed" }
It’s pretty simple. Given a Media Library item, post the binary to http://api.tinify.com. Take the resulting optimized image URL (returned from the API), and set the original media item’s binary to the newly optimized version.
Of course, the Kraken.io version is similar. The only difference really is that the Kraken API requires an image URL which they will then fetch and optimize. This may or may not work for you depending on your network setup. It also requires a bit of imagination to properly generate a URL resolving to an image inside of the master database (e.g., optimize the image directly in master before publishing to web). This is just a quick and dirty example of how it can be achieved:
function KrakImage($mediaItem) { $muo = New-Object Sitecore.Resources.Media.MediaUrlOptions $muo.AlwaysIncludeServerUrl = 1 $muo.UseItemPath = 0 $muo.AbsolutePath = 1 # May or may not work depending on your setup. You basically need $linkUrl to be a URL pointing to your image. # The trick is that your image may or may not be published, so you probably need $linkUrl to resolve to the master # database if possible. $linkUrl = [Sitecore.Resources.Media.MediaManager]::GetMediaUrl($mediaItem, $muo) $url = $linkUrl -replace '/sitecore/shell', '' # replace with your api credentials: https://kraken.io/docs/getting-started $krakenKey = "KRAKEN KEY" $krakenSecret = "KRAKEN SECRET" $hash = @{ auth = @{ api_key = $krakenKey; api_secret = $krakenSecret; }; wait = $TRUE; url = $url; } $jsonBody = $hash | convertto-json Try { $request = Invoke-RestMethod -Method Post -Uri https://api.kraken.io/v1/url -ContentType "application/json" -Body $jsonBody if ($request.success -eq $TRUE) { return $request.kraked_url } else { Write-Error "Kraken request failed" Write-Host ($request | Format-List | Out-String) } } Catch { Write-Error "Kraken request failed" Break } } # same SetMedia method as the TinyPNG example function SetMedia($mediaItem, $url, $extension) { # ... } $location = get-location $scItem = Get-Item $location $mediaItem = New-Object "Sitecore.Data.Items.MediaItem" $scItem $extension = $mediaItem.Extension $url = KrakImage($mediaItem) if ($location) { Write-Host "Kraken optimized url: $url" SetMedia $mediaItem $url $extension } else { Write-Error "Kraken.io failed" }
If you set all of this up correctly, you should now have an option like this when right clicking on an image in the Media Library:
Shoutout to CJ Morgan for advocating PowerShell at Brainjocks, now Perficient, as well as Michael West and Adam Najmanowicz for being a tremendous help in Sitecore Slack!
Enjoy!
Dylan, I stumbled across your post while searching for something else, but knew we had a specific request to do this exact thing with several thousand images. I had a far more laborious solution in mind. You just saved me a long, painful, soul-crushing slog. The only change I’m making is to create batches so I can load up a bunch at once. Thanks for sharing this.
Glad it worked out for you Chris!
Pingback: Auto-Setting Alt Text For Existing & New Images with Cognitive Services - Flux Digital Blog
Hi,
I tried to use your solution because I have some heavy images. The problem is that when I try to use the image with parameters mw and mh I have a really pixelated image that is not usable. Do you know why it happens ?
Hi Joey,
Typically I would recommend uploading images to the media library which are already optimized and of the appropriate size. The mw and mh properties instruct Sitecore to programmatically resize the images, which I believe is done via a built in .NET mechanic. I doubt that this would give crisp results.
Also, if you use TinyPNG- it is technically a lossy style compression meaning that it will compress the image at some visual clarity loss. Since it is a lossy style compression, you can run the optimization multiple times on the same image. If you do this, each subsequent optimization will lead to a slightly more pixelated image. In general, you should only ever run TinyPNG’s optimization on an image once.