Resize image and preserve ratio with Powershell

My recently created company website Keluro has two blogs: one in french and one in english. The main blog pages are a preview list of the existing posts. I gave the possibility to the writer to put a thumbnail image in the preview. It’s a simply <img /> tag where a css class is responsible for the resizing and to display the image in a 194px X 194px box while keeping the original aspect ratio. Most of the time this preview is a reduction of an image that is displayed in the blog post. Everything was fine until I found out that the these blog pages did not received a good mark while inspecting them with PageSpeedInsights . It basically says that the thumbnails were not optimized enough… For SEO reasons I want these blog pages to load quickly so I needed to resize these images once for all even if it has to duplicate the image.

Resizing and keeping aspect ratio

Resizing two pictures with landscape and portrait aspect ratio to make them fill a given canvas.

I think that most of us already had to do such kind of image resizing task. You can use many available software to do that: Paint, Office Picture Manager, Gimp, Inkscape etc. However, when it comes to manipulate many pictures, it could be really useful to use a script. Let me share with you this Powershell script that you can use to resize your .jpg pictures. Note that there is also a quality parameter (from 1 to 100) that you can use if you need to compress more the image.

Param ( [Parameter(Mandatory=$True)] [ValidateNotNull()] $imageSource,
[Parameter(Mandatory=$True)] [ValidateNotNull()] $imageTarget,
[Parameter(Mandatory=$true)][ValidateNotNull()] $quality )

if (!(Test-Path $imageSource)){throw( "Cannot find the source image")}
if(!([System.IO.Path]::IsPathRooted($imageSource))){throw("please enter a full path for your source path")}
if(!([System.IO.Path]::IsPathRooted($imageTarget))){throw("please enter a full path for your target path";)}
if ($quality -lt 0 -or $quality -gt 100){throw( "quality must be between 0 and 100.")}

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$bmp = [System.Drawing.Image]::FromFile($imageSource)

#hardcoded canvas size...
$canvasWidth = 194.0
$canvasHeight = 194.0

#Encoder parameter for image quality
$myEncoder = [System.Drawing.Imaging.Encoder]::Quality
$encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
$encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($myEncoder, $quality)
# get codec
$myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()|where {$_.MimeType -eq 'image/jpeg'}

#compute the final ratio to use
$ratioX = $canvasWidth / $bmp.Width;
$ratioY = $canvasHeight / $bmp.Height;
$ratio = $ratioY
if($ratioX -le $ratioY){
  $ratio = $ratioX
}

#create resized bitmap
$newWidth = [int] ($bmp.Width*$ratio)
$newHeight = [int] ($bmp.Height*$ratio)
$bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
$graph = [System.Drawing.Graphics]::FromImage($bmpResized)

$graph.Clear([System.Drawing.Color]::White)
$graph.DrawImage($bmp,0,0 , $newWidth, $newHeight)

#save to file
$bmpResized.Save($imageTarget,$myImageCodecInfo, $($encoderParams))
$graph.Dispose()
$bmpResized.Dispose()
$bmp.Dispose()

Now suppose that you have saved and named the script above “MakePreviewImages.ps1”. You may use it in a loop statement such as the following one where we assume that MakePreviewImages.ps1 is located under the current directory and the images are in a subfolder called “images”.

Get-ChildItem .images -Recurse -Include *.jpg | Foreach-Object{
   $newName = $_.FullName.Substring(0, $_.FullName.Length - 4) + "_resized.jpg"
     ./MakePreviewImages.ps1 $_.FullName $newName 75
   }

13 thoughts on “Resize image and preserve ratio with Powershell

  1. Ilya

    Awesome script, works like a charm!

    I had to replace all ‘&quot’ to ‘ symbols and that it worked fine

    Reply
  2. Develop3r

    Thanks for the code works great, I want to change the target output to a new folder by setting $imageTarget any thoughts on how I would achieve this

    Reply
    1. benoitpatra Post author

      Yes.
      First your extract the folder full path from the target file name and if it does not exist you create it.
      You can insert something like that at the beginning of MakePreviewImages.ps1

      $folderPath = [System.IO.Path]::GetDirectoryName($targetPath)
      if(!(Test-Path $folderPath)){
         New-Item $folderPath -ItemType Directory
      }
      
      Reply
  3. beebleep

    Thanks for this awesome code, does exactly what i needed! Do you btw know a way to save to the same file i.e. overwrite the source file instead of creating a new one?
    Can I also change the location of the script?

    Get-ChildItem c:\totally\some\where\else -Recurse -Include *.jpg | Foreach-Object{
    $newName = $_.FullName.Substring(0, $_.FullName.Length – 4) + “_resized.jpg”
    c:\mylocation\MakePreviewImages.ps1 $_.FullName $newName 75
    }

    I tried the above but it resulted in ‘out of memory’ errors

    Reply
    1. benoitpatra Post author

      Remind that with Powershell you have access to the .NET framework. This is the case here, we access real .NET Bitmap classes. So the same methods and techniques are available.
      It looks like that the Save() method does not contain a parameter allowing override of an image. You need to delete it on the disk first
      http://stackoverflow.com/questions/8905714/overwrite-existing-image

      I am surprised of your Out of Memory errors. In the script you mention, the images are processed one by one. There is probably a memory leak.
      I am sorry but I do not have time to investigate right now.
      Can you try adding $graph.Dispose() before $bmpResized.Dispose() ?

      Reply
  4. Andrew

    Hi,

    I have current folder as C:\Data\ImageResizeTest but seems to check under Users for the images. Which part of the script sets the source path? So it can be passed as a string?

    Get-ChildItem : Cannot find path ‘C:\Users\akelly\.images’ because it does not exist.

    Thank you!
    Andrew

    Reply
  5. Benoit Patra Post author

    Actually the original script is a procedure (or function) that writes the image at the variable $imageTarget.

    In the latter script I give a small example : here the script recursively resizes all pictures in under the “.images” folder located the current directory. So there is no place where the script looks for “Users” directory especially.

    I think that your command line current directory is C:\Users\akelly (that usually the default) and if you have taken my second script “as-is” it will also look for the “.images” directory that does not exist on your machine. This explains your error.

    What you probably want is to ask the script to look into your target directory, just tell it to do so:

    Get-ChildItem "C:\Data\ImageResizeTest" -Recurse -Include *.jpg | Foreach-Object{
    $newName = $_.FullName.Substring(0, $_.FullName.Length – 4) + "_resized.jpg"
    ./MakePreviewImages.ps1 $_.FullName $newName 75
    }

    Reply
    1. Andrew

      Thanks for reply. I’ve now got it working from any required folder and call the code as a function since I want it all on one script. I require images to be under 1000x1000px. And this works great thanks for those larger on both x and y axis. (Example 1194x2189px resized to 545x1000px)

      But what I currently need is the follow…

      1) The code won’t resize an image where one axis is already under the resize number (in my case 1000px). So an image 600×1500 will not resize using height 1000 x width 1000. Can the script be changed to do the resize?
      2) Also as per the working example, even though 1000×1000 was hard-code it stayed in proportion and didn’t stretch which is great, but is there an option to add the white space so it is the 1000×1000 as specified.
      3) How can I change this to work for png files?

      Above is what is important but next on my list to work out is…

      4) Do you have any code which can add an additional 30px of white around an image?
      5) Do you have any script to remove white and make a transparent png?

      Thanks
      Andrew

      Function ResizeImage(){
      Param ( [Parameter(Mandatory=$True)] [ValidateNotNull()] $imageSource,
      [Parameter(Mandatory=$True)] [ValidateNotNull()] $imageTarget,
      [Parameter(Mandatory=$true)][ValidateNotNull()] $quality )

      if (!(Test-Path $imageSource)){throw( “Cannot find the source image”)}
      if(!([System.IO.Path]::IsPathRooted($imageSource))){throw(“please enter a full path for your source path”)}
      if(!([System.IO.Path]::IsPathRooted($imageTarget))){throw(“please enter a full path for your target path”)}
      if ($quality -lt 0 -or $quality -gt 100){throw( “quality must be between 0 and 100.”)}

      [void][System.Reflection.Assembly]::LoadWithPartialName(“System.Drawing”)
      $bmp = [System.Drawing.Image]::FromFile($imageSource)

      #hardcoded canvas size…
      $canvasWidth = 1000.0
      $canvasHeight = 1000.0

      #Encoder parameter for image quality
      $myEncoder = [System.Drawing.Imaging.Encoder]::Quality
      $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
      $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($myEncoder, $quality)
      # get codec
      $myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()|where {$_.MimeType -eq ‘image/jpeg’}

      #compute the final ratio to use
      $ratioX = $canvasWidth / $bmp.Width;
      $ratioY = $canvasHeight / $bmp.Height;
      $ratio = $ratioY
      if($ratioX -le $ratioY){
      $ratio = $ratioX
      }

      #create resized bitmap
      $newWidth = [int] ($bmp.Width*$ratio)
      $newHeight = [int] ($bmp.Height*$ratio)
      $bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
      $graph = [System.Drawing.Graphics]::FromImage($bmpResized)

      $graph.Clear([System.Drawing.Color]::White)
      $graph.DrawImage($bmp,0,0 , $newWidth, $newHeight)

      #save to file
      $bmpResized.Save($imageTarget,$myImageCodecInfo, $($encoderParams))
      $graph.Dispose()
      $bmpResized.Dispose()
      $bmp.Dispose()
      }

      $imageSourcePath = “C:\Data\ImageResizeTest\images”
      $imageExt = “jpg”
      $newImageLabel = “_resized”

      Get-ChildItem $imageSourcePath -Recurse -Include *.$imageExt | Foreach-Object{
      $newName = $_.FullName.Substring(0, $_.FullName.Length – 4) + “$newImageLabel.$imageExt”

      ResizeImage $_.FullName $newName 100
      }

      Reply
      1. Andrew

        Can ignore 1) it does resize where one side is under the specified size. This image was a PNG which wasn’t being picked up because I hadn’t solved that bit yet!

        I should learn to write one message at the end of my day, I know!

        Reply
  6. Andrew

    Resolved 3) and 5)

    I’m not sure why it didn’t work the first time I tried but..
    – switching MineType image/jpeg to image/png
    – Change $imageExt = “png”

    Change “White” to “Transparent” (But only works for png as jpg created with black background)
    $graph.Clear([System.Drawing.Color]::Transparent)

    I don’t PS so a learning curve for me! Running in debug mode line by line in VS Code is helping a lot.

    Reply
  7. Andrew

    Actually I didn’t solve 5). Using Transparent only retains transparency. It doesn’t make a white background transparent 🙁

    Reply
    1. Benoit Patra Post author

      Hi Andrew,
      Sorry for late reply. Actually I do not think the image manipulation .NET libraries we are using can do 5). Removing transparency is one thing but adding transparency is another. Indeed, it needs to be “smart somewhere” to make such a change.

      For sure this can be done by any image manipulation software such as Gimp: https://www.gimp.org/. I do not know any commandline solution that could do that out-of-the-box. You may want to have a look at the imagemagick library (https://www.imagemagick.org/).

      Reply

Leave a Reply to Develop3r Cancel reply

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