PNGCrush all

Some programs are great to use but are annoying when it comes to bulk operations they are just too cumbersome, PNGCrush is one of them.
Don’t get me wrong, I really like this program (and actually use it quite often), but it needs a bit of spice to be more useful in our busy daily lives. I don’t want to figure out the syntax every usage and windows “scripting” is just to unreliable.

DotNet to the rescue!, if there is one thing this framework is useful for it’s fast and reliable programming, so a simple CLI helper is quickly formed.

  • Can easily be adapted for other programs/use cases
  • Launches one PNCrush instance per core
  • Displays files that will be processed
  • Displays neatly formatted results
  • Finds the best available PNGCrush executable (version and architecture)
  • Works with DotNet 4.5.2 and newer, older versions require some tinkering (although I don’t see valid reasons to remain on any older version)
  • Could be converted to .AsParallel() LinQ, but where is the fun in that?
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CrushAll
{
    public class Program
    {
        public static void Main(string[] args)
        {
            _args = args;
            try
            {
                CrushAll();
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception.ToString());
            }

            Console.WriteLine("Done, press any key to exit");
            Console.ReadLine();
        }

        private static string[] _args;
        private static readonly List ActiveTasks = new List();
        private static string _pngCrush = "";
        private static string _currentPath = "";
        private static void CrushAll()
        {
            _currentPath = AppDomain.CurrentDomain.BaseDirectory;
            List files = ReturnFiles(_currentPath, true, new List { ".png" }).ToList();
            files.AddRange(_args.Where(argument => argument.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && File.Exists(argument)));
            IEnumerable executables = ReturnFiles(_currentPath, true, new List { ".exe" });

            // ReSharper disable PossibleMultipleEnumeration
            if (!files.Any())
            {
                Console.WriteLine("No images found!");
                return;
            }
            if (!executables.Any())
            {
                Console.WriteLine("No PNG Crush executable(s) found!");
                return;
            }

            List possibleExecutables = new List();
            foreach (string file in executables.Select(Path.GetFileName).Where(file => file.StartsWith("pngcrush_", StringComparison.OrdinalIgnoreCase)))
            {
                try
                {
                    possibleExecutables.Add(new PossibleExecutable { X64 = file.Contains("_w64"), Version = Version.Parse(file.Substring(file.IndexOf("_", StringComparison.Ordinal) + 1, file.LastIndexOf("_", StringComparison.Ordinal) - (file.IndexOf("_", StringComparison.Ordinal) + 1)).Replace("_", ".")), Executable = file});
                }
                catch (Exception) {}
            }
            // ReSharper restore PossibleMultipleEnumeration

            Version highestX64 = new Version(0, 0);
            Version highestX86 = new Version(0, 0);
            foreach (PossibleExecutable executable in possibleExecutables)
            {
                if (executable.Version > highestX86 && !executable.X64)
                {
                    highestX86 = executable.Version;
                }
                if (executable.Version > highestX64 && executable.X64)
                {
                    highestX64 = executable.Version;
                }
            }
            
            if (highestX86 > highestX64 && Environment.Is64BitOperatingSystem)
            {
                foreach (PossibleExecutable executable in possibleExecutables.Where(executable => executable.Version == highestX86))
                {
                    _pngCrush = executable.Executable;
                    break;
                }
            }
            else if (highestX64.Major != 0 && Environment.Is64BitOperatingSystem)
            {
                foreach (PossibleExecutable executable in possibleExecutables.Where(executable => executable.Version == highestX64 && executable.X64))
                {
                    _pngCrush = executable.Executable;
                    break;
                }
            }
            else if (highestX86.Major != 0)
            {
                foreach (PossibleExecutable executable in possibleExecutables.Where(executable => executable.Version == highestX86 && !executable.X64))
                {
                    _pngCrush = executable.Executable;
                    break;
                }
            }

            if (string.IsNullOrEmpty(_pngCrush))
            {
                Console.WriteLine("No suitable executable found!");
                return;
            }

            Console.WriteLine("Preparing to crush:");
            Console.WriteLine("Thread(s): " + Environment.ProcessorCount.ToString(CultureInfo.InvariantCulture));
            Console.WriteLine("Executable: " + _pngCrush.Replace(_currentPath, ""));

            Queue queue = new Queue();
            foreach (string file in files)
            {
                queue.Enqueue(file);
                Console.WriteLine("File: " + file.Replace(_currentPath, ""));
            }
            
            while (queue.Any())
            {
                Thread.Sleep(500);

                lock (ActiveTasks)
                {
                    while (queue.Any())
                    {
                        if (Environment.ProcessorCount <= ActiveTasks.Count) break;
                        string file = queue.Dequeue();
                        Task task = Task.Factory.StartNew(() => { ProcessFile(file); }, TaskCreationOptions.LongRunning);
                        task.ContinueWith(t => HandleException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
                        task.ContinueWith(DoneProcessing, TaskContinuationOptions.None);
                        ActiveTasks.Add(task);
                    }
                }
            }

            while (true)
            {
                lock (ActiveTasks)
                {
                    Thread.Sleep(500);
                    if (!ActiveTasks.Any())
                    {
                        break;
                    }
                }
            }
        }
        private static void HandleException(AggregateException exception)
        {
            foreach (Exception ex in exception.Flatten().InnerExceptions)
            {
                Console.WriteLine(ex.ToString());
            }
        }
        private static void DoneProcessing(Task task)
        {
            lock (ActiveTasks)
            {
                ActiveTasks.Remove(task);
            }
        }

        public static void ProcessFile(string file)
        {
            string tempFile = file + ".tmp";
            Int64 orriginalLength, newLength;
            using (FileStream fileInfo = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Write))
            {
                orriginalLength = fileInfo.Length;
            }
            
            // ReSharper disable once AssignNullToNotNullAttribute
            ProcessStartInfo processStartInfo = new ProcessStartInfo { FileName = _pngCrush, Arguments = "-brute -q -reduce \"" + file + "\" \"" + file + ".tmp\"", WorkingDirectory = Path.GetDirectoryName(_pngCrush), CreateNoWindow = true, UseShellExecute = false };
            Process process = Process.Start(processStartInfo);
            if (process != null)
            {
                process.WaitForExit();
                process.Close();
                process.Dispose();
            }
            else
            {
                Console.WriteLine("Could not start PNGCrush!");
                return;
            }

            if (!File.Exists(tempFile))
            {
                Console.WriteLine("Could not recompress: " + file.Replace(_currentPath, ""));
                return;
            }

            using (FileStream fileInfo = File.Open(tempFile, FileMode.Open, FileAccess.Read, FileShare.Write))
            {
                newLength = fileInfo.Length;
            }
            
            if (newLength > orriginalLength)
            {
                Console.WriteLine(file.Replace(_currentPath, "") + ": resize was bigger, ignoring");
            }
            else if (newLength == orriginalLength)
            {
                Console.WriteLine(file.Replace(_currentPath, "") + ": resize was equal, ignoring");
            }
            else
            {
                Console.WriteLine(file.Replace(_currentPath, "") + ": reduced by " + FormatBytes(orriginalLength - newLength));

                int retries = 0;

                // File operations can be a bit quirky in DotNet, sometimes the file handle isn't released on time so we wait a bit and retry
retry:
                File.Delete(file);
                if (File.Exists(file))
                {
                    if (retries > 3)
                    {
                        Console.WriteLine(file.Replace(_currentPath, "") + ": could not be deleted");
                        return;
                    }
                    Thread.Sleep(250);
                    retries++;
                    goto retry;
                }

                retries = 0;

retry2:
                File.Move(tempFile, file);
                if (!File.Exists(file))
                {
                    if (retries > 3)
                    {
                        Console.WriteLine(file.Replace(_currentPath, "") + ": could not recreated");
                        return;
                    }
                    Thread.Sleep(250);
                    retries++;
                    goto retry2;
                }
            }

            // Don't remove the tempFile if the original no longer exists
            if (File.Exists(tempFile) && File.Exists(file))
            {
                File.Delete(tempFile);
            }
        }
        
        public static IEnumerable ReturnFiles(string localPath, bool recurseDirectories, List includedExtensions = null)
        {
            if (!Directory.Exists(localPath)) return new List();

            try
            {
                List directories = new List();
                if (recurseDirectories)
                {
                    directories.AddRange(RecurseDirectories(localPath).ToArray());
                }
                else
                {
                    directories.Add(localPath);
                }

                List list = new List();
                foreach (string directory in directories)
                {
                    DirectoryInfo directoryInfo = new DirectoryInfo(directory);
                    list.AddRange(from file in directoryInfo.GetFiles() where !(includedExtensions != null && !includedExtensions.Contains(file.Extension, StringComparer.OrdinalIgnoreCase)) select directory + file);
                }
                return list;
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception.ToString());
                return new List();
            }
        }
        public static IEnumerable RecurseDirectories(string localPath)
        {
            List directoriesReturn = new List();
            try
            {
                string[] directories = Directory.GetDirectories(localPath, "*", SearchOption.AllDirectories);
                directoriesReturn.AddRange(directories.Reverse());
                directoriesReturn.Add(localPath);
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception.ToString());
            }

            return directoriesReturn;
        }

        private static readonly string[] Sizes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
        public static string FormatBytes(Int64 bytes)
        {
            int order = 0;
            double size = bytes;
            while (size >= 1024 && order + 1 < Sizes.Length)
            {
                order++;
                size = size / 1024;
            }
            return $"{size:0.##}{Sizes[order]}";
        }
    }

    public class PossibleExecutable
    {
        public string Executable;
        public bool X64;
        public Version Version;
    }
}

PNGCrush-All