Recently I had the task of compressing a load of JPEG images stored in Microsoft Azure Blob Storage down to a manageable size, without losing image quality (or at least not to the naked eye). Mozilla has recently released mozjpeg, which is a command line JPEG-encoder tool that boasts a far greater compression efficiency than other JPEG encoders. Many SEO agencies and of course the Google PageSpeed tool will suggest that in order to optimize JPEG images for the web, you should strip `EXIF` data from them and also compress them to within an inch of their lives. Now JPEG isn't the best image format by a country mile; I'm a `PNG` man myself. However, `JPEG` is the one that is used most widely on the web, and Mozilla's rationale is whilst the general consensus moves towards better image formats, they wanted to make the transition away from JPEG a little more manageable. I was looking for a way of compressing hundreds of images programatically, as well as removing `EXIF` metadata from these images at the same time. The mozjpeg download comes loaded with various executables. The two `.exes` we are interested in are `jpegtran` and `cjpeg` - `jpegtran` will do the legwork to remove `EXIF` metadata and `cjpeg` will do the lossless JPEG compression. The Sitch So say we have a bunch of JPEG files stored locally in the root of your ASP.NET site - say in a folder called `myImages`. What we need to do is make use of the `System.Diagnostics.Process` class to launch a command prompt on the server machine programatically. Step I. The `jpegtran` Utility - Removing `EXIF` metadata As you can see from the sample code below, I have installed the mozjpeg library at the path `C:Program Filesmozjpeg`:

var jpegtran = new Process() {
               StartInfo = {
                 Arguments = jpegtranargs,
                 FileName = @"C:Program Filesmozjpegjpegtran.exe",
                 UseShellExecute = false,
                 CreateNoWindow = false,
                 WindowStyle = ProcessWindowStyle.Maximized,
                 RedirectStandardError = true,
                 RedirectStandardOutput = true
               }
           };

The above code will start a command prompt using the given filename, which is the path to the `jpegtran` executable. Take note that I am also passing some arguments to the executable, which will look something like the below (they will include an input and output path as well as other options):

string jpegtranargs = " -copy none -outfile "" + Server.MapPath("~/myImages/") + theOutString + """ + " "" + Server.MapPath("~/myImages/") + theOutString + """;

For the purposes of this demo, we are going to process the same file - so the input file will have it's EXIF data removed and no new JPEGs will be created. We need to call `.Start()` on the process:

jpegtran.Start();

And then we need to wait for the process to finish and exit:

jpegtran.WaitForExit();

If you go to the containing folder where the original JPEG is and open it up in PhotoShop, you will see that it's EXIF metadata has been stripped. The full `jpegtran` code can be found below. The `CreateProcess` method is used by both `JPEG` utilities to launch a new process with the correct executable name:

/// <summary>
/// Removes EXIF and other metadata from the given JPEG file.
/// </summary>
/// <param name="pathToImage">Use Server.MapPath to get a reference to the image</param>
/// <returns>Indicates whether the stripping of metadata has been successful</returns>
public static bool StripMetadata(string pathToImage)
{
    bool wasSuccessful;

    try
    {
        string jpegtranargs = " -copy none -outfile "" + pathToImage + """ + " "" + pathToImage + """;

        Process jpegTranProcess = CreateProcess("jpegtran", jpegtranargs);

        jpegTranProcess.Start();
        jpegTranProcess.WaitForExit();

        wasSuccessful = true;
    }
    catch (Exception ex)
    {
        SendExceptionEmail(ex);
        wasSuccessful = false;
    }
    return wasSuccessful;
}

/// <summary>
/// Creates a new System.Diagnostics.Process instance with the matching file path to an executable, and using the given args.
/// </summary>
/// <param name="executableName">The name of the executable, without the .exe</param>
/// <param name="args">The string of arguments</param>
/// <returns>Returns a new instance of System.Diagnostics.Process</returns>
private static Process CreateProcess(string executableName, string args)
{
    var proc = new Process
    {
        StartInfo =
        {
            Arguments = args,
            FileName = pathToExe + executableName + ".exe",
            UseShellExecute = false,
            CreateNoWindow = false,
            WindowStyle = ProcessWindowStyle.Hidden,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        }
    };

    return proc;
}

Step II. The `cjpeg` utility - Lossless compression So now that we have removed the EXIF metadata from the target image, we now need to use the `cjpeg` utility to compress the given image losslessly. We can use the `CreateProcess` method to start the `cjpeg.exe` executable. The arguments we need to pass across for `cjpeg` include `-quality 60`, the full output path (this is the same as in step I) using the `-outfile [PATH]` argument and finally the input file path (no argument flag needed). The final argument string will look a bit like this:

`-quality 60 -outfile "C:UsersAdamDocumentsTestImagesmyjpeg.jpeg" "C:UsersAdamDocumentsTestImagesmyjpeg.jpeg"

The final methods concerned with `cjpeg` compression can be found below:

/// <summary>
/// Compresses image using mozjpeg (cjpeg), uploads new image to s3 bucket and returns url of newly uploaded image
/// </summary>
/// <param name="pathToImage">Use Server.MapPath to get a reference to the image</param>
/// <param name="compressionQuality">Defaults to 60%, however can be changed to anything (10%+ is advised)</param>
/// <returns>Indicates whether the method has been successful</returns>
public static bool CompressImage(string pathToImage, int compressionQuality = quality)
{
    bool wasSuccessful;
    try
    {
        string qualityArgs = " -quality " + quality; // quality is default property
        string theDirectoryName = Path.GetDirectoryName(pathToImage);
        string theExtension = Path.GetExtension(pathToImage);
        string theFileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathToImage);

        string outfileArgs = " -outfile "" + theDirectoryName + Path.DirectorySeparatorChar + theFileNameWithoutExtension + "-lossless" + theExtension + """;

        string theFinalArgsToUse = string.Format("{0} {1} {2}", qualityArgs, outfileArgs, pathToImage);

        Process cjpeg = CreateProcess("cjpeg", theFinalArgsToUse);

        cjpeg.Start();
        cjpeg.WaitForExit();

        wasSuccessful = true;
    }
    catch (Exception ex)
    {
        SendExceptionEmail(ex);
        wasSuccessful = false;
    }
    return wasSuccessful;
}

Final remarks

  • Compression quality defaults to 60%, however can be changed to anything (10% of the original is advised to preserve "lossless-ness").
  • The savings in `kbs` can be pretty radical.
  • Make sure you've gotten the path to the `mozjpeg` directory right!
  • Any questions or problems, then just leave a comment below.