Optimize your images in Sitecore with PowerShell Extensions + Kraken or TinyPNG

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.

tinypng splash image

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.

kraken.io splash image

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:

how to add script to sitecore

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:

Optimize Image script in Action

Shoutout to CJ Morgan for advocating PowerShell at BrainJocks, as well as Michael West and Adam Najmanowicz for being a tremendous help in Sitecore Slack!

Enjoy!

Dylan McCurry

I am a certified Sitecore developer with a passion for the web. I hopped into the .NET space 5 years ago to work on enterprise-class applications and never looked back. I love building things—everything from from Legos to software that solves real problems. I have a strong foundation of backend skills, with sweet spots like security, portal solutions and APIs. Early on, before I had the benefit of SCORE, I made a lot of mistakes with Sitecore but learned a lot in the course of the struggle. I would like to support other developers by contributing my perspective on doing things “the Sitecore way,” rather than fighting the framework. Did I mention I love video games? Learn more about Dylan McCurry.

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back