Author: gagan
Date: Thu Feb  3 17:15:45 2011
New Revision: 1066875

URL: http://svn.apache.org/viewvc?rev=1066875&view=rev
Log:
Patch by satya3656 | Issue 3082041: Support for retaining subsampling parameter 
for jpeg image rewrite | http://codereview.appspot.com/3082041/

Modified:
    shindig/trunk/java/common/conf/shindig.properties
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java

Modified: shindig/trunk/java/common/conf/shindig.properties
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/common/conf/shindig.properties?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- shindig/trunk/java/common/conf/shindig.properties (original)
+++ shindig/trunk/java/common/conf/shindig.properties Thu Feb  3 17:15:45 2011
@@ -72,6 +72,7 @@ shindig.image-rewrite.max-palette-size =
 shindig.image-rewrite.allow-jpeg-conversion = true
 shindig.image-rewrite.jpeg-compression = 0.90
 shindig.image-rewrite.min-threshold-bytes = 200
+shindig.image-rewrite.jpeg-retain-subsampling = false
 # Huffman optimization reduces the images size by addition 4-6% without
 # any loss in the quality of the image, but takes extra cpu cycles for
 # computing the optimized huffman tables.

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
 Thu Feb  3 17:15:45 2011
@@ -22,10 +22,15 @@ import org.apache.sanselan.ImageWriteExc
 import org.apache.sanselan.Sanselan;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
 
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
-import com.google.common.base.Objects;
 
+import com.sun.imageio.plugins.jpeg.JPEG;
 import com.sun.imageio.plugins.jpeg.JPEGImageWriter;
 
 import java.awt.image.BufferedImage;
@@ -40,8 +45,9 @@ import javax.imageio.ImageIO;
 import javax.imageio.ImageTypeSpecifier;
 import javax.imageio.ImageWriteParam;
 import javax.imageio.ImageWriter;
-import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
+import javax.imageio.metadata.IIOInvalidTreeException;
 import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
 
 /**
  * Base class for image optimizers
@@ -59,13 +65,19 @@ abstract class BaseOptimizer {
   protected ImageOutputter outputter;
   protected byte[] minBytes;
   protected int minLength;
+  protected JpegImageUtils.JpegImageParams sourceImageParams;
   int reductionPct;
 
-
   public BaseOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    this(config, response, null);
+  }
+
+  public BaseOptimizer(OptimizerConfig config, HttpResponseBuilder response,
+                       JpegImageUtils.JpegImageParams sourceImageParams) {
     this.config = config;
     this.response = response;
     this.minLength = response.getContentLength();
+    this.sourceImageParams = sourceImageParams;
     this.outputter = getOutputter();
   }
 
@@ -82,7 +94,12 @@ abstract class BaseOptimizer {
                 config.getJpegHuffmanOptimization());
         }
       }
-      return new ImageIOOutputter(writer, param);
+
+      JpegImageUtils.SamplingModes samplingMode = 
JpegImageUtils.SamplingModes.DEFAULT;
+      if (config.getJpegRetainSubsampling() && sourceImageParams != null) {
+        samplingMode = sourceImageParams.getSamplingMode();
+      }
+      return new ImageIOOutputter(writer, param, samplingMode);
     }
     return new 
SanselanOutputter(FORMAT_NAME_TO_IMAGE_FORMAT.get(getOriginalFormatName()));
   }
@@ -94,6 +111,7 @@ abstract class BaseOptimizer {
     if (image == null) {
       return;
     }
+    
     byte[] bytes = outputter.toBytes(image);
     if (minLength > bytes.length) {
       minBytes = bytes;
@@ -156,9 +174,17 @@ abstract class BaseOptimizer {
     ImageWriter writer;
     ByteArrayOutputStream baos;
     ImageWriteParam writeParam;
+    JpegImageUtils.SamplingModes jpegSamplingMode;
+
     public ImageIOOutputter(ImageWriter writer, ImageWriteParam writeParam) {
+      this(writer, writeParam, JpegImageUtils.SamplingModes.DEFAULT);
+    }
+
+    public ImageIOOutputter(ImageWriter writer, ImageWriteParam writeParam,
+                            JpegImageUtils.SamplingModes jpegSamplingMode) {
       this.writer = writer;
       this.writeParam = Objects.firstNonNull(writeParam, 
writer.getDefaultWriteParam());
+      this.jpegSamplingMode = jpegSamplingMode;
     }
 
     public byte[] toBytes(BufferedImage image) throws IOException {
@@ -185,11 +211,55 @@ abstract class BaseOptimizer {
       IIOMetadata metadata = writer.getDefaultImageMetadata(
           new ImageTypeSpecifier(image.getColorModel(), 
image.getSampleModel()),
           metaImageWriteParam);
+      
+      if (jpegSamplingMode.getModeValue() > 0 && writer instanceof 
JPEGImageWriter) {
+        setJpegSubsamplingMode(metadata);
+      }
+
       writer.write(null, new IIOImage(image, 
Collections.<BufferedImage>emptyList(), metadata),
                    metaImageWriteParam == null ? writeParam : null);
 
       return baos.toByteArray();
     }
+
+    private void setJpegSubsamplingMode(IIOMetadata metadata)
+        throws IIOInvalidTreeException {
+      // Tweaking the image metadata to override default subsampling(4:2:0) 
with
+      // 4:4:4.
+      Node rootNode = metadata.getAsTree(JPEG.nativeImageMetadataFormatName);
+      boolean metadataUpdated = false;
+      // The top level root node has two children, out of which the second one 
will
+      // contain all the information related to image markers.
+      if (rootNode.getLastChild() != null) {
+        Node markerNode = rootNode.getLastChild();
+        NodeList markers = markerNode.getChildNodes();
+        // Search for 'SOF' marker where subsampling information is stored.
+        for (int i = 0; i < markers.getLength(); i++) {
+          Node node = markers.item(i);
+          // 'SOF' marker can have
+          //   1 child node if the color representation is greyscale,
+          //   3 child nodes if the color representation is YCbCr, and
+          //   4 child nodes if the color representation is YCMK.
+          // This subsampling applies only to YCbCr.
+          if (node.getNodeName().equalsIgnoreCase("sof") && 
node.hasChildNodes() &&
+              node.getChildNodes().getLength() == 3) {
+            // In 'SOF' marker, first child corresponds to the luminance 
channel, and setting
+            // the HsamplingFactor and VsamplingFactor to 1, will imply 4:4:4 
chroma subsampling.
+            NamedNodeMap attrMap = node.getFirstChild().getAttributes();
+            int samplingMode = jpegSamplingMode.getModeValue();
+            attrMap.getNamedItem("HsamplingFactor").setNodeValue((samplingMode 
& 0xf) + "");
+            
attrMap.getNamedItem("VsamplingFactor").setNodeValue(((samplingMode >> 4) & 
0xf) + "");
+            metadataUpdated = true;
+            break;
+          }
+        }
+      }
+
+      // Read the updated metadata from the metadata node tree.
+      if (metadataUpdated) {
+        metadata.setFromTree(JPEG.nativeImageMetadataFormatName, rootNode);
+      }
+    }
   }
 
   /**

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
 Thu Feb  3 17:15:45 2011
@@ -304,6 +304,11 @@ public class BasicImageRewriter implemen
         return;
       }
 
+      JpegImageUtils.JpegImageParams jpegImageParams = null;
+      if (imageFormat == ImageFormat.IMAGE_FORMAT_JPEG) {
+        jpegImageParams = 
JpegImageUtils.getJpegImageData(response.getContentBytes(), uri.getPath());
+      }
+
       // Step#1: Read the image using appropriate readers for the 
corresponding image format.
       BufferedImage image = readImage(imageFormat, response);
 
@@ -324,7 +329,7 @@ public class BasicImageRewriter implemen
 
       // Step#5: Optimize the supported image formats viz PNG, GIF, JPG & BMP 
using 'BaseOptimizer'
       // and it's subclass implementations for the above four formats.
-      applyOptimizer(response, imageFormat, image, config);
+      applyOptimizer(response, imageFormat, jpegImageParams, image, config);
     } catch (IOException ioe) {
       if (LOG.isLoggable(Level.WARNING)) {
         LOG.logp(Level.WARNING, classname, "rewrite", 
MessageKeys.IO_ERROR_REWRITING_IMG, new Object[] 
{request.toString(),ioe.getMessage()});
@@ -433,7 +438,8 @@ public class BasicImageRewriter implemen
   }
 
   protected void applyOptimizer(HttpResponseBuilder response, ImageFormat 
imageFormat,
-      BufferedImage image, OptimizerConfig config) throws IOException {
+      JpegImageUtils.JpegImageParams jpegImageParams, BufferedImage image,
+      OptimizerConfig config) throws IOException {
     if (imageFormat == ImageFormat.IMAGE_FORMAT_GIF) {
       // Detecting the existence of the NETSCAPE2.0 extension by string 
comparison
       // is not exactly clean but is good enough to determine if a GIF is 
animated
@@ -444,7 +450,7 @@ public class BasicImageRewriter implemen
     } else if (imageFormat == ImageFormat.IMAGE_FORMAT_PNG) {
       new PNGOptimizer(config, response).rewrite(image);
     } else if (imageFormat == ImageFormat.IMAGE_FORMAT_JPEG) {
-      new JPEGOptimizer(config, response).rewrite(image);
+      new JPEGOptimizer(config, response, jpegImageParams).rewrite(image);
     } else if (imageFormat == ImageFormat.IMAGE_FORMAT_BMP) {
       new BMPOptimizer(config, response).rewrite(image);
     }

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
 Thu Feb  3 17:15:45 2011
@@ -57,6 +57,11 @@ public class JPEGOptimizer extends BaseO
     super(config, response);
   }
 
+  public JPEGOptimizer(OptimizerConfig config, HttpResponseBuilder response,
+                       JpegImageUtils.JpegImageParams sourceImageParams) {
+    super(config, response, sourceImageParams);
+  }
+
   @Override
   protected void rewriteImpl(BufferedImage image) throws IOException {
     int pngLength = Integer.MAX_VALUE;
@@ -64,7 +69,8 @@ public class JPEGOptimizer extends BaseO
       // Create a new optimizer config and disable JPEG conversion
       OptimizerConfig pngConfig = new 
OptimizerConfig(config.getMaxInMemoryBytes(),
           config.getMaxPaletteSize(), false, config.getJpegCompression(),
-          config.getMinThresholdBytes(), config.getJpegHuffmanOptimization());
+          config.getMinThresholdBytes(), config.getJpegHuffmanOptimization(),
+          config.getJpegRetainSubsampling());
 
       // Output the image as PNG
       PNGOptimizer pngOptimizer = new PNGOptimizer(pngConfig, response);

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
 Thu Feb  3 17:15:45 2011
@@ -31,6 +31,7 @@ public class OptimizerConfig {
   private final float jpegCompression;
   private final int minThresholdBytes;
   private final boolean jpegHuffmanOptimization;
+  private final boolean jpegRetainSubsampling;
 
   @Inject
   public OptimizerConfig(
@@ -39,7 +40,8 @@ public class OptimizerConfig {
       @Named("shindig.image-rewrite.allow-jpeg-conversion") boolean 
jpegConversionAllowed,
       @Named("shindig.image-rewrite.jpeg-compression") float jpegCompression,
       @Named("shindig.image-rewrite.min-threshold-bytes") int 
minThresholdBytes,
-      @Named("shindig.image-rewrite.jpeg-huffman-optimization") boolean 
jpegHuffmanOptimization) {
+      @Named("shindig.image-rewrite.jpeg-huffman-optimization") boolean 
jpegHuffmanOptimization,
+      @Named("shindig.image-rewrite.jpeg-retain-subsampling") boolean 
jpegRetainSubsampling) {
     this.maxInMemoryBytes = maxInMemoryBytes;
     this.maxPaletteSize = maxPaletteSize;
     this.jpegConversionAllowed = jpegConversionAllowed;
@@ -48,13 +50,14 @@ public class OptimizerConfig {
     this.jpegCompression = Math.min(0.9f,Math.max(0.5f, jpegCompression));
     this.minThresholdBytes = minThresholdBytes;
     this.jpegHuffmanOptimization = jpegHuffmanOptimization;
+    this.jpegRetainSubsampling = jpegRetainSubsampling;
   }
 
   /**
    * Defaults for usage in tests.
    */
   public OptimizerConfig() {
-    this(1024 * 1024, 256, true, 0.90f, 200, false);
+    this(1024 * 1024, 256, true, 0.90f, 200, false, false);
   }
 
   /**
@@ -102,4 +105,12 @@ public class OptimizerConfig {
   public boolean getJpegHuffmanOptimization() {
     return jpegHuffmanOptimization;
   }
+
+  /**
+   * Indicate if we want to do retian original jpeg subsampling while encoding 
the jpeg's.
+   */
+  public boolean getJpegRetainSubsampling() {
+    return jpegRetainSubsampling;
+  }
+
 }

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
 Thu Feb  3 17:15:45 2011
@@ -48,6 +48,28 @@ public class JPEGOptimizerTest extends B
   }
 
   @Test
+  public void testRetainSubsmaplingDisabled() throws Exception {
+    HttpResponse resp =
+        
createResponse("org/apache/shindig/gadgets/rewrite/image/testImage444.jpg", 
"image/jpeg");
+    HttpResponse rewritten = rewrite(resp, getConfigWithRetainSampling(false, 
0.70f));
+    JpegImageUtils.JpegImageParams params =
+        JpegImageUtils.getJpegImageData(rewritten.getResponse(), "");
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+    assertEquals(JpegImageUtils.SamplingModes.YUV420, 
params.getSamplingMode());
+  }
+
+  @Test
+  public void testRetainSubsmaplingEnabled() throws Exception {
+    HttpResponse resp =
+        
createResponse("org/apache/shindig/gadgets/rewrite/image/testImage444.jpg", 
"image/jpeg");
+    HttpResponse rewritten = rewrite(resp, getConfigWithRetainSampling(true, 
0.70f));
+    JpegImageUtils.JpegImageParams params =
+        JpegImageUtils.getJpegImageData(rewritten.getResponse(), "");
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+    assertEquals(JpegImageUtils.SamplingModes.YUV444, 
params.getSamplingMode());
+  }
+
+  @Test
   public void testLargeJPEG() throws Exception {
     HttpResponse resp =
         createResponse("org/apache/shindig/gadgets/rewrite/image/large.jpg", 
"image/jpeg");
@@ -115,8 +137,9 @@ public class JPEGOptimizerTest extends B
   HttpResponse rewrite(HttpResponse original, OptimizerConfig config)
       throws IOException, ImageReadException {
     HttpResponseBuilder builder = new HttpResponseBuilder(original);
-    new JPEGOptimizer(config, builder).rewrite(
-        JPEGOptimizer.readJpeg(original.getResponse()));
+    new JPEGOptimizer(config, builder,
+                      
JpegImageUtils.getJpegImageData(builder.getContentBytes(), ""))
+        .rewrite(JPEGOptimizer.readJpeg(original.getResponse()));
     return builder.create();
   }
 
@@ -125,6 +148,14 @@ public class JPEGOptimizerTest extends B
     return new OptimizerConfig(defaultConfig.getMaxInMemoryBytes(),
     defaultConfig.getMaxPaletteSize(), false,
     defaultConfig.getJpegCompression(), defaultConfig.getMinThresholdBytes(),
-    defaultConfig.getJpegHuffmanOptimization());
+    defaultConfig.getJpegHuffmanOptimization(), 
defaultConfig.getJpegRetainSubsampling());
+  }
+
+  OptimizerConfig getConfigWithRetainSampling(boolean enabled, float quality) {
+    OptimizerConfig defaultConfig = new OptimizerConfig();
+    return new OptimizerConfig(defaultConfig.getMaxInMemoryBytes(),
+       defaultConfig.getMaxPaletteSize(), 
defaultConfig.isJpegConversionAllowed(),
+       quality, defaultConfig.getMinThresholdBytes(), 
defaultConfig.getJpegHuffmanOptimization(),
+       enabled);
   }
 }
\ No newline at end of file

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java?rev=1066875&r1=1066874&r2=1066875&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java
 Thu Feb  3 17:15:45 2011
@@ -29,19 +29,19 @@ public class OptimizerConfigTest {
 
   @Test
   public void testForHighJpegCompression() {
-    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
1.00f, 200, false);
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
1.00f, 200, false, false);
     assertEquals(0.9f, config.getJpegCompression(), 0.0001);
   }
 
   @Test
   public void testForLowJpegCompression() {
-    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
0.10f, 200, false);
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
0.10f, 200, false, false);
     assertEquals(0.5f, config.getJpegCompression() , 0.0001);
   }
 
   @Test
   public void testForAcceptableJpegCompression() {
-    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
0.85f, 200, false);
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 
0.85f, 200, false, false);
     assertEquals(0.85f, config.getJpegCompression(), 0.0001);
   }
 }


Reply via email to