imagepowershellimage-conversion

Trouble converting PNG to scaled Bitmap


I am trying to convert a PNG to a scaled bitmap. The end goal is to scale the image maintaining aspect ratio to be a height of 165 pixels. The following code works fine, and I get the image I expect as a bitmap, although it only reduces the resolution by half and does not calculate what the scale should be:

# Current test file is 256x256
$InFile = '.\someImage.png'

# Read in the initial image as FileStream
[System.IO.FileStream]$imageStream = [System.IO.FileStream]::new(
    "$(Resolve-Path $InFile)",
    [System.IO.FileMode]::Open,
    [System.IO.FileAccess]::Read,
    [System.IO.FileShare]::Read
)

# Read image FileStream into BitmapSource
$bitmapSource = [System.Windows.Media.Imaging.PngBitmapDecoder]::new(
    $imageStream,
    [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat,
    [System.Windows.Media.Imaging.BitmapCacheOption]::Default
).Frames[0]

$rotatedBitmap = [System.Windows.Media.Imaging.TransformedBitmap]::new(
    $bitmapSource,
    [System.Windows.Media.RotateTransform]::new(-90)
)

# Scale down the image so it isn't too large
$scale = .5

$scaledBitmap = [System.Windows.Media.Imaging.TransformedBitmap]::new(
    $rotatedBitmap,
    [System.Windows.Media.ScaleTransform]::new($scale, $scale)
)

# Create new bitmap from rotated/scaled bitmap using BGRA32 pixel format
$convertedBitmap = [System.Windows.Media.Imaging.FormatConvertedBitmap]::new()
$convertedBitmap.BeginInit()
$convertedBitmap.Source = $scaledBitmap
$convertedBitmap.DestinationFormat = [System.Windows.Media.PixelFormats]::Bgra32
$convertedBitmap.EndInit()

# Prepare transformed bitmap for writing via FileStream
$pixels = [byte[]]::new(( $convertedBitmap.PixelWidth * $convertedBitmap.PixelHeight * 4 ))
$stride = $convertedBitmap.PixelWidth * 4 + ( $convertedBitmap.PixelWidth % 4 )
$convertedBitmap.CopyPixels($pixels, $stride, 0)

However, if I use a different value for the scale, I get an error on the final line of the block. For example, changing the scale like so:

# Change the $scale = value in the previous code block to this
# for capping the height at 165 pixels and execute to reproduce
$scale = 165 / $rotatedBitmap.PixelHeight

Causes $convertedBitmap.CopyPixels to throw an error:

Exception calling "CopyPixels" with "3" argument(s): "The parameter value cannot be less than '109064'.
Parameter name: buffer"
At C:\Users\bender\OneDrive\Documents\src\personal\MyProject\Test-Function.ps1:89 char:9
+         $convertedBitmap.CopyPixels($pixels, $stride, 0)
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentOutOfRangeException

Why does my code break when I change the scaling? My initial research suggests either the $stride is wrong or the $pixels buffer is incorrectly sized, but I'm not sure how that is the case since I calculate both from the scaled bitmap, and it works with other scaling values (or omitting scaling altogether).


Solution

  • Just remove the term + ( $convertedBitmap.PixelWidth % 4 ). As you have converted the bitmap to BGRA32 format, you only need to multiply the pixel width by 4 to get a stride (width in bytes) that is a multiple of 4, as required:

    $stride = $convertedBitmap.PixelWidth * 4
    

    Just for completeness, if you had used the input bitmap as-is, so it could be other bit depths than 32, the formula should be:

    $stride = (((($bitmap.PixelWidth * $bitmap.Format.BitsPerPixel) + 31) -band -bnot 31) -shr 3)
    

    I haven't tested it though, I just ported the formula from this doc to powershell.