Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save thomas694/d41f7a9c6d7d4b9c227dd45302c20379 to your computer and use it in GitHub Desktop.

Select an option

Save thomas694/d41f7a9c6d7d4b9c227dd45302c20379 to your computer and use it in GitHub Desktop.
Convert PNG files in folder tree to JPEG format

Convert PNG files in folder tree to JPEG format

The script converts all PNG files in the specified folder tree to JPEG format running four workers in parallel

The attached powershell script searches all PNG files in the specified folder (including its subfolders) and converts them to JPEG, omitting PNG files with alpha channel used. If there is already a conversion from a previous run, the file is skipped. The filenames of the converted files get the extension for the target format appended.

For a conversion to JPEG format the following steps and their commands are used.

Determine if the PNG contains an alpha channel:

$out = C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\identify -format '%[channels]' "$path"
if ($out -eq "rgba" -or $out -eq "srgba") {

In case it contains one, the script checks, if it is used or not (opaque):

$out = C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\convert "$path" -format "%[opaque]" info:

The PNG files without an alpha channel are first converted to a 24-bit PNG:

C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\convert $path $path_new24

and then via bisection method converted to JPEG files with a compression value between 70 and 100:

$result = C:\Tools\_mozjpeg-v4.0.3-win-x64\cjpeg.exe -quality $quality -outfile "$fn_jpg" "$path" 2>&1

until the visual difference between the original PNG and the converted JPEG is below a defined threshold of 1%:

$output = C:\Tools\_ssim-master\rmgr-ssim.exe -y "$path" "$fn_jpg"
...

To avoid having to repeat the steps for PNG files with an alpha channel, the files are tagged in the Alternate Data Stream (ADS) of the file. This allows the file to be easily identified and skipped during the next run.

You need to adjust the paths in the script to your installation folders of ImageMagick, Mozilla JPEG Encoder and ssim.
In lines 110-111 of the file convert_png_to_jpg.ps1 you can specify folders to exclude and a pattern for filenames to exclude (e.g. filename.skip.png).

The script makes use of an example implementation of producer / consumer parallelism in PowerShell by Lee Holmes.

<#PSScriptInfo
.VERSION 1.0
.GUID bfb939b9-03f0-433e-ad0f-e4e12f4a009c
.AUTHOR Lee Holmes
#>
<#
.DESCRIPTION
Example implementation of producer / consumer parallelism in PowerShell
#>
[Console]::TreatControlCAsInput = $true
if (!$args[0]) {
Write-Host "No Folder specified!"
Exit
}
## The script block we want to run in parallel. Threads will all
## retrieve work from $InputQueue, and send results to $OutputQueue
$parallelScript = {
param(
## An Input queue of work to do
$InputQueue,
## The output buffer to write responses to
$OutputQueue,
## State tracking, to help threads communicate
## how much progress they've made
$OutputProgress, $ThreadId, $ShouldExit,
## Script location
$ScriptPath
)
## Continually try to fetch work from the input queue, until
## the 'ShouldExit' flag is set
$processed = 0
$workItem = $null
while(! $ShouldExit.Value)
{
if ([Console]::KeyAvailable){
$readkey = [Console]::ReadKey($true)
if ($readkey.Modifiers -eq "Control" -and $readkey.Key -eq "C"){
$OutputQueue.Enqueue("Ctrl-C pressed...")
return
}
}
if($InputQueue.TryDequeue([ref] $workItem))
{
## If we got a work item, do something with it.
$worker_name = "worker${PID}_$ThreadId"
$script = "$ScriptPath\convert_png_to_jpg_worker.ps1"
#Try
#{
#Invoke-Expression "$script $worker_name `"$workItem`""
& $script $worker_name $workItem
#}
#Catch
#{
# $OutputQueue.Enqueue($_.Exception.Message)
#}
$workItemResult = "$LastExitCode $workItem"
## Add the result to the output queue
$OutputQueue.Enqueue($workItemResult)
## Update our progress
$processed++
$OutputProgress[$ThreadId] = $processed
}
else
{
## If there was no work, wait a bit for more.
Start-Sleep -m 100
}
}
}
## Create a set of background PowerShell instances to do work, based on the
## number of available processors.
#$threads = Get-WmiObject Win32_Processor | Foreach-Object NumberOfLogicalProcessors
$threads = 4
$runspaces = 1..$threads | Foreach-Object { [PowerShell]::Create() }
$outputProgress = New-Object 'Int[]' $threads
$inputQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
$outputQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
$shouldExit = $false
$scriptPath = $PSScriptRoot
## Spin up each of our PowerShell runspaces. Once invoked, these are actively
## waiting for work and consuming once available.
for($counter = 0; $counter -lt $threads; $counter++)
{
$null = $runspaces[$counter].AddScript($parallelScript).
AddParameter("InputQueue", $inputQueue).
AddParameter("OutputQueue", $outputQueue).
AddParameter("OutputProgress", $outputProgress).
AddParameter("ThreadId", $counter).
AddParameter("ShouldExit", [ref] $shouldExit).
AddParameter("ScriptPath", $scriptPath).BeginInvoke()
}
## Qeueu some work
$path = $args[0]
$estimated = 0
foreach ($item in Get-ChildItem $path -Recurse -Filter *.png)
{
if (!$item.FullName.Contains("\filepaths_to_exclude\") -and
!$item.FullName.Contains(".skip"))
{
$currentInput = $item.FullName
$inputQueue.Enqueue($currentInput)
$estimated++
}
}
## Wait for our worker threads to complete processing the
## work.
try
{
do
{
## Update the status of how many items we've processed, based on adding up the
## output progress from each of the worker threads
$totalProcessed = $outputProgress | Measure-Object -Sum | Foreach-Object Sum
if ($estimated -gt 0)
{
Write-Progress "Processed $totalProcessed of $estimated" -PercentComplete ($totalProcessed * 100 / $estimated)
}
## If there were any results, output them.
$scriptOutput = $null
while($outputQueue.TryDequeue([ref] $scriptOutput))
{
$scriptOutput
}
## If the threads are done processing the input we gave them, let them know they can exit
if($inputQueue.Count -eq 0)
{
$shouldExit = $true
}
Start-Sleep -m 100
## See if we still have any busy runspaces. If not, exit the loop.
$busyRunspaces = $runspaces | Where-Object { $_.InvocationStateInfo.State -ne 'Complete' }
} while($busyRunspaces)
}
finally
{
## Clean up our PowerShell instances
foreach($runspace in $runspaces)
{
$runspace.Stop()
$runspace.Dispose()
}
}
[Console]::TreatControlCAsInput = $true
if (!$args[0]) {
Write-Host "No File specified!"
Exit -1
}
$path = $args[0]
$target_ssim = 0.99
$min_jpg_quality = 70
$max_jpg_quality = 100
#foreach ($item in Get-ChildItem $path -Recurse -Filter *.png)
$item = Get-ChildItem -LiteralPath $path
#{
Write-Host "$($item.FullName)"
$fn_jpg = $item.FullName + ".jpg"
if (!$item.FullName.ToLower().Contains(".skip") -and !(Test-Path -LiteralPath $fn_jpg -PathType Leaf)) {
#check if already tried before
$stream = Get-Item -LiteralPath $path -Stream PngConv -ErrorAction Ignore
if ($stream) {
$stream = Get-Content -LiteralPath $path -Stream PngConv
if ($stream -eq 1) {
Exit -5 #converted jpg not saving enough
}
elseif ($stream -eq -2) {
Exit -2 #alpha channel detected
}
}
# check for pngs with alpha channel
$out = C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\identify -format '%[channels]' "$path"
if ($out -eq "rgba" -or $out -eq "srgba") {
# check if alpha channel is really used
$out = C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\convert "$path" -format "%[opaque]" info:
if ($out -eq "False") {
Write-Host "skipping image with alpha: $path"
#mark png as already checked
Set-Content -LiteralPath $path -Stream PngConv -Value "-2"
Exit -2
}
$path_new = $path.substring(0, $path.length-3) + "new.png"
if (Test-Path -LiteralPath $path_new -PathType Leaf) {
Write-Host "tmp file already exists: $path_new"
Exit -3
}
Write-Host $path_new
$path_new24 = "png24:" + $path_new
C:\Tools\ImageMagick-7.1.0-portable-Q16-HDRI-x64\convert $path $path_new24
$path = $path_new
}
if (Test-Path -LiteralPath "$fn_jpg.~" -PathType Leaf) {
Write-Host "new file already exists: $fn_jpg.~"
Exit -4
}
[int]$quality = ($min_jpg_quality + $max_jpg_quality) / 2
$tmpPath = ""
while ($min_jpg_quality -ne $max_jpg_quality) {
Write-Host "quality: $quality"
if (Test-Path -LiteralPath "$fn_jpg" -PathType Leaf) {
if (Test-Path -LiteralPath "$fn_jpg.~" -PathType Leaf) { Remove-Item -LiteralPath "$fn_jpg.~" }
Rename-Item -LiteralPath "$fn_jpg" "$fn_jpg.~"
}
$result = C:\Tools\mozjpeg-v4.0.3-win-x64\cjpeg.exe -quality $quality -outfile "$fn_jpg" "$path" 2>&1
if ($result) { $result = $result.ToString() }
if ($tmpPath -eq "" -and $result.Contains("cjpeg.exe: can't open")) {
$guid = [guid]::NewGuid().ToString()
$tmpPath = Split-Path -Path $path
$tmpPath = "$tmpPath\$guid.png"
Copy-Item -LiteralPath $path -Destination $tmpPath
$path = $tmpPath
$fn_jpg = $path + ".jpg"
C:\Tools\mozjpeg-v4.0.3-win-x64\cjpeg.exe -quality $quality -outfile "$fn_jpg" "$path"
}
$output = C:\Tools\ssim-master\rmgr-ssim.exe -y "$path" "$fn_jpg"
$value = [decimal]$output
Write-Host "Value: $value"
Write-Host "$min_jpg_quality $quality $max_jpg_quality"
if ($value -ge $target_ssim) {
$max_jpg_quality = $quality
if ($min_jpg_quality -eq $max_jpg_quality -and (Test-Path -LiteralPath "$fn_jpg.~" -PathType Leaf)) {
if (Test-Path -LiteralPath "$fn_jpg.~" -PathType Leaf) {
Remove-Item -LiteralPath "$fn_jpg.~"
}
}
} else {
$min_jpg_quality = $quality + 1
if ($min_jpg_quality -eq $max_jpg_quality -and (Test-Path -LiteralPath "$fn_jpg.~" -PathType Leaf)) {
Write-Host "using old file"
Move-Item -LiteralPath "$fn_jpg.~" "$fn_jpg" -force
}
}
if ($min_jpg_quality + 1 -eq $max_jpg_quality) {
$quality = $min_jpg_quality
} else {
[int]$quality = ($min_jpg_quality + $max_jpg_quality) / 2
}
}
Write-Host "chosen quality: $quality"
if ($tmpPath -ne "") {
$origJpg = $item.FullName + ".jpg"
Rename-Item -LiteralPath $fn_jpg -NewName $origJpg
Remove-Item -LiteralPath $tmpPath
$fn_jpg = $origJpg
}
if ($path_new) {
Remove-Item -LiteralPath $path_new
}
# set time to original file's one
$oldDate = $item.LastWriteTimeUtc
$jpg = Get-ChildItem -LiteralPath $fn_jpg
if ($item.LastWriteTimeUtc -ne $jpg.LastWriteTimeUtc) {
$jpg.LastWriteTimeUtc = $oldDate
}
}
#}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment