16 February 2011

How to change JPEG compression in Clojure

Not just how to change JPEG compression, but to do so without losing the JPEG's metadata. That's the problem I had to figure out for this program I'm writing in Clojure. I found out and wrote about how to do this in Java yesterday. Finally, here's the same thing in Clojure:

Short explanation of what the code is doing is interwoven with the code here. The code without the comments (for easy copy/paste'ing) is at bottom.

First, we import a few Java classes:

(require 'clojure.java.io)
(import [javax.imageio IIOImage ImageIO]
        [javax.imageio.plugins.jpeg JPEGImageWriteParam])
(try

Then we get the default JPEG image reader and writer:

(let [image-reader (.next (ImageIO/getImageReadersByFormatName "jpg"))
      image-writer (.next (ImageIO/getImageWritersByFormatName "jpg"))]

Now we open up streams to the input and output JPEG Files. Note that with-open will close the streams for you, so you don't have to later.

(with-open [image-input-stream (ImageIO/createImageInputStream 
                                 (clojure.java.io/file "path/to/inputFile.jpg"))
            image-output-stream (ImageIO/createImageOutputStream 
                                 (clojure.java.io/file "path/to/outputFile.jpg"))]

Next, mate the reader/writer to the respective streams:

(.setInput image-reader image-input-stream)
(.setOutput image-writer image-output-stream)

Then we'll get the JPEG input file into a container that will also contain the metadata:

(let [iio-image (IIOImage. (.read image-reader 0) nil
                             (.getImageMetadata image-reader 0))

Now set up the JPEG quality (ie, compression) level desired. Here it's set to 0.7 (where 1 is highest quality, and 0 is highest compression):

jpeg-params (doto (.getDefaultWriteParam image-writer)
               (.setCompressionMode JPEGImageWriteParam/MODE_EXPLICIT)
               (.setCompressionQuality 0.7))]

Finally we get to write out the JPEG file with the new compression level:

(.write image-writer nil iio-image jpeg-params)))

Lastly, make sure to clean up the reader/writer's so they don't continue to hog system resources (yes, you have to do this even though Java has garbage collection):

(finally
 (do (.dispose image-writer)
     (.dispose image-reader)))))

To recap, here it is again without the comments, just the bare code:


(require 'clojure.java.io)
(import [javax.imageio IIOImage ImageIO]
        [javax.imageio.plugins.jpeg JPEGImageWriteParam])
(try
  (let [image-reader (.next (ImageIO/getImageReadersByFormatName "jpg"))
        image-writer (.next (ImageIO/getImageWritersByFormatName "jpg"))]
    (with-open [image-input-stream (ImageIO/createImageInputStream 
                                     (clojure.java.io/file "path/to/inputFile.jpg"))
                image-output-stream (ImageIO/createImageOutputStream 
                                     (clojure.java.io/file "path/to/outputFile.jpg"))]
      (.setInput image-reader image-input-stream)
      (.setOutput image-writer image-output-stream)
      (let [iio-image (IIOImage. (.read image-reader 0) nil
                                   (.getImageMetadata image-reader 0))
            jpeg-params (doto (.getDefaultWriteParam image-writer)
                           (.setCompressionMode JPEGImageWriteParam/MODE_EXPLICIT)
                           (.setCompressionQuality 0.7))]
         (.write image-writer nil iio-image jpeg-params)))
    (finally
       (do (.dispose image-writer)
           (.dispose image-reader)))))

I should warn you that I haven't tested the above code exactly as it is. I factored it into pieces so that I could use it in what I wrote differently, but the above is the smallest coherent example, I suppose. So you'll have to adapt the above. Eg, the require and import should be in the (ns ...).

Hope that helps!

(Edit: Fixed a few minor errors, including one pointed out by klang in comments below.)

3 comments:

klang said...

I have tested the code and wrapped it up in a gist .. am I missing something about the use of 'finally' in the clojure program? The image-reader/writer variables are out of scope when finally is called.

klang said...

This gist represent a testet version of the code above.

mlsci said...

You are absolutely right about image-reader/writer going out of scope. I just realized there's a few other minor mistakes.

Sorry!! Thanks for making the tested version.

I'm going to have to fix this post and/or put up a link to the gist later.