You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by li...@apache.org on 2010/12/15 19:38:10 UTC

svn commit: r1049663 - in /shindig/trunk/java/gadgets/src: main/java/org/apache/shindig/gadgets/rewrite/image/ test/java/org/apache/shindig/gadgets/rewrite/image/ test/resources/org/apache/shindig/gadgets/rewrite/image/

Author: lindner
Date: Wed Dec 15 18:38:10 2010
New Revision: 1049663

URL: http://svn.apache.org/viewvc?rev=1049663&view=rev
Log:
Patch from MHarish Mnsatya | Adding a utility class for detecting Jpeg image attributes (e.g. compression quality

Added:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java
    shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg
    shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg
    shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java?rev=1049663&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java Wed Dec 15 18:38:10 2010
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.common.BinaryFileParser;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.apache.sanselan.formats.jpeg.JpegUtils;
+import org.apache.sanselan.formats.jpeg.JpegConstants;
+
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.plugins.jpeg.JPEGQTable;
+
+/**
+ * Utility functions for jpeg image introspection.
+ */
+public class JpegImageUtils {
+  private static final Logger LOG = Logger.getLogger(ImageUtils.class.getName());
+  private static final int END_OF_IMAGE_MARKER = 0xffd9;
+  private static final String INVALID_JPEG_ERROR_MSG = "Not a Valid JPEG File";
+  private static final int HUFFMAN_TABLE_MARKER = 0xffc4;
+  private static final int QUANTIZATION_TABLE_MARKER = 0xffdb;
+  private static final int MAX_DC_SYMBOLS = 12;
+  private static final int MAX_AC_SYMBOLS = 162;
+
+  private JpegImageUtils() {}
+
+  /**
+   * Various subsampling modes supported by Jpeg and the corresponding values for
+   * this integer.
+   *   4:4:4 subsampling -> 0x11 -> 17
+   *   4:2:2 subsampling -> 0x21 -> 33
+   *   4:2:0 subsampling -> 0x22 -> 34
+   *   4:1:1 subsampling -> 0x41 -> 65
+   */
+  public static enum SamplingModes {
+    UNKNOWN(-2),
+    DEFAULT(-1),
+    YUV444(17),
+    YUV422(33),
+    YUV420(34),
+    YUV411(65);
+
+    private SamplingModes(int mode) {
+      this.mode = mode;
+    }
+
+    public int getModeValue() {
+      return mode;
+    }
+
+    private int mode;
+  }
+
+  public static class JpegImageParams {
+    private SamplingModes mode;
+    private boolean huffmanOptimized;
+    private float approxQualityFactor;
+    private float lumaQualityFactor = -1;
+    private float chromaQualityFactor = -1;
+
+    private final int[] k1LumaQuantTable = JPEGQTable.K1Luminance.getTable();
+    private final int[] k2ChromaQuantTable = JPEGQTable.K2Chrominance.getTable();
+
+    private int[][] tables = new int[2][64];
+    private int lumaIndex = -1;
+    private int chromaIndex = -1;
+
+    JpegImageParams(SamplingModes mode, boolean huffmanOptimized, float approxQualityFactor) {
+      this.mode = mode;
+      this.huffmanOptimized = huffmanOptimized;
+      this.approxQualityFactor = approxQualityFactor;
+    }
+
+    public SamplingModes getSamplingMode() {
+      return mode;
+    }
+
+    public void setSamplingMode(int samplingMode) {
+      for (SamplingModes mode : SamplingModes.values()) {
+        if (samplingMode == mode.getModeValue()) {
+          this.mode = mode;
+          return;
+        }
+      }
+      
+      mode = SamplingModes.UNKNOWN;
+      LOG.log(Level.WARNING, "Unable to read subsampling information for Jpeg Image");
+    }
+
+    public boolean isHuffmanOptimized() {
+      return huffmanOptimized;
+    }
+
+    public void setHuffmanOptimized(boolean huffmanOptimized) {
+      this.huffmanOptimized = huffmanOptimized;
+    }
+
+    public void setLumaIndex(int index) {
+      this.lumaIndex = index;
+    }
+
+    public void setChromaIndex(int index) {
+      this.chromaIndex = index;
+    }
+
+    /**
+     * Quality is defined in terms of the base quantization tables used by encoder.
+     * Q = quant table, q = compression quality  and S = table used by encoder,
+     * Encoder does the following.
+     * if q > 0.5 then Q = 2 - 2*q*S otherwise Q = (0.5/q)*S.
+     *
+     * Since we dont have access to the table used by encoder. But it is generally close
+     * to the standard table defined by JPEG. Hence, we approx by taking sum of all values
+     * of the standard JPEG table and comparing with sum of all values of quant table.
+     *
+     * @param table quantization table specified in the jpeg header.
+     * @param stdTable reference quantization table specified in jpeg standard.
+     * @return approximate compression quality which lies in interval [0.0, 1.0].
+     */
+    public float approximateQuality(int[] table, int[] stdTable) {
+      int total = 0;
+      int stdTotal = 0;
+      for (int i = 0; i < 64; i++) {
+        total += table[i];
+        stdTotal += stdTable[i];
+      }
+
+      float scaleFactor = 0;
+      scaleFactor = (total - 32F)/stdTotal;
+
+      float approxChannelQuality;
+      if (scaleFactor > 1.0) {
+        approxChannelQuality = 0.5F / scaleFactor;
+      } else {
+        approxChannelQuality = (2.0F - scaleFactor) / 2.0F;
+      }
+      return approxChannelQuality;
+    }
+
+    /**
+     * Adds quantization table to image data.
+     *
+     * @param tableIndex quantization table index.
+     * @param table quantization table that is used in while encoding.
+     */
+    public void addQTable(int tableIndex, int[] table) {
+      if (tableIndex == 0 || tableIndex == 1) {
+        tables[tableIndex] = Arrays.copyOf(table, table.length);
+      }
+    }
+
+    public float getChromaQualityFactor() {
+      if (chromaQualityFactor < 0 && chromaIndex >= 0) {
+        chromaQualityFactor = approximateQuality(tables[chromaIndex], k2ChromaQuantTable);
+      }
+      return chromaQualityFactor;
+    }
+
+    public float getLumaQualityFactor() {
+     if (lumaQualityFactor < 0 && lumaIndex >= 0) {
+        lumaQualityFactor = approximateQuality(tables[lumaIndex], k1LumaQuantTable);
+      }
+      return lumaQualityFactor;
+    }
+
+    public float getApproxQualityFactor() {
+      if (approxQualityFactor < 0) {
+        approxQualityFactor = (getLumaQualityFactor() + 2 * getChromaQualityFactor()) / 3.0F;
+      }
+
+      return approxQualityFactor;
+    }
+  }
+
+  /**
+   * This function tries to extract various information from jpeg image like subsampling, jpeg
+   * compression quality and whether huffman optimzation is applied on the image data.
+   *
+   * @param is input stream comprisng the image data.
+   * @param filename of the image.
+   */
+  public static JpegImageParams getJpegImageData(InputStream is, String filename)
+      throws IOException, ImageReadException {
+    final JpegImageParams imageParams = new JpegImageParams(SamplingModes.UNKNOWN, false, -1);
+
+    JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
+      BinaryFileParser binaryParser = new BinaryFileParser();
+
+      // return false to exit before reading image data.
+      public boolean beginSOS() {
+        return false;
+      }
+
+      public void visitSOS(int marker, byte markerBytes[],
+          byte imageData[]) {
+      }
+
+      // return false to exit traversal.
+      public boolean visitSegment(int marker, byte markerBytes[], int markerLength,
+          byte markerLengthBytes[], byte segmentData[]) throws ImageReadException, IOException {
+
+        if (marker == END_OF_IMAGE_MARKER)
+          return false;
+
+        if ((marker == JpegConstants.SOF0Marker) || (marker == JpegConstants.SOF2Marker)) {
+          parseSOFSegment(markerLength, segmentData);
+        } else if (marker == HUFFMAN_TABLE_MARKER) {
+          parseHuffmanTables(markerLength, segmentData);
+        } else if (marker == QUANTIZATION_TABLE_MARKER) {
+          parseQuantizationTables(markerLength, segmentData);
+        }
+
+        return true;
+      }
+
+      /**
+       * This function tries to extract the subsampling information from the JPEG image using
+       * either 'SOF0' or 'SOF2' segment.
+       * The structure of the 'SOF' marker is as follows.
+       *   - data precision (1 byte) in bits/sample,
+       *   - image height (2 bytes, little endian),
+       *   - image width (2 bytes, little endian), 
+       *   - number of components (1 byte), usually 1 = grey scaled, 3 = color YCbCr
+       *     or YIQ, 4 = color CMYK)
+       *   - for each component: 3 bytes
+       *     - component id (1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q)
+       *     - sampling factors (bit 0-3 vertical sampling, 4-7 horizontal sampling)
+       *     - quantization table index
+       *
+       * @param markerLength length of the SOF marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseSOFSegment(int markerLength, byte[] segmentData)
+          throws IOException, ImageReadException {
+        // parse the SOF Marker.
+        int toBeProcessed = markerLength - 2;
+        int numComponents = 0;
+        InputStream is = new ByteArrayInputStream(segmentData);
+        
+        // Skip precision(1 Byte), height(2 Bytes), width(2 bytes) bytes.
+        if (toBeProcessed > 6) {
+          binaryParser.skipBytes(is, 5, INVALID_JPEG_ERROR_MSG);
+          numComponents = binaryParser.readByte("Number_of_components", is,
+              "Unable to read Number of components from SOF marker");
+          toBeProcessed -= 6;
+        } else {
+          LOG.log(Level.WARNING, "Failed to SOF marker");
+          return;
+        }
+
+        // TODO(satya): Extend this library to gray scale images.
+        if (numComponents == 3 && toBeProcessed == 9) {
+          // Process 'Luma' Channel.
+          // Skipping the component Id field.
+          binaryParser.skipBytes(is, 1, INVALID_JPEG_ERROR_MSG);
+          imageParams.setSamplingMode(binaryParser.readByte("Sampling Factors", is,
+              "Unable to read the sampling factor from the 'Y' channel component spec"));
+          imageParams.setLumaIndex(binaryParser.readByte("Quantization Table Index", is,
+              "Unable to read Quantization table index of 'Y' channel"));
+
+          // Process 'Chroma' Channel.
+          // Skipping the component Id and sampling factor fields.
+          binaryParser.skipBytes(is, 2, INVALID_JPEG_ERROR_MSG);
+          imageParams.setChromaIndex(binaryParser.readByte("Quantization Table Index", is,
+              "Unable to read Quantization table index of 'Cb' Channel"));
+        } else {
+          LOG.log(Level.WARNING, "Failed to Component Spec from SOF marker");
+        }
+      }
+
+
+      /**
+       * This function tries to parse the Quantizations tables and adds them to JpegImageData
+       * object. If segmentData has more bytes after parsing first QT, that means DQT segment has
+       * multiple quantization tables. We allow multiple quant tables to have same tableIndex,
+       * and the latter one overrides the previous one. we currently parse upto 2 quantization
+       * tables. 
+       * The structure of the DQT (Define Quantization Table) segment.
+       *   - QT information (1 byte): (bit 0 = LSB and bit 7 = MSB)
+       *     bit 3..0: index of QT (3..0, otherwise error)
+       *     bit 7..4: precision of QT, 0 means 8 bit, 1 means 16 bit, otherwise bad input
+       *   - n bytes QT values, n = 64*(precision+1)
+       *
+       * @param markerLength length of the DQT marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseQuantizationTables(int markerLength, byte[] segmentData)
+          throws ImageReadException, IOException {
+        InputStream is = new ByteArrayInputStream(segmentData);
+        int toBeProcessed = markerLength - 2;
+        while (toBeProcessed > 1) {
+          int tableInfo = binaryParser.readByte("Quantization Table Info", is,
+                                                "Not able to read Quantization Table Info");
+          toBeProcessed--;
+          int tableIndex = tableInfo & 0x0f;
+          int precision = tableInfo >> 4;
+          if (toBeProcessed < 64*(precision + 1)) {
+            return;
+          }
+
+          int[] quanTable = new int[64];
+          for (int i = 0; i < 64; i++) {
+            quanTable[i] = (precision == 0) ?
+                binaryParser.readByte("Reading", is, "Reading Quanization Table Failed") :
+                binaryParser.read2Bytes("Reading", is, "Reading Quantization Table Failed");
+          }
+          imageParams.addQTable(tableIndex, quanTable);
+          toBeProcessed -= 64*(precision + 1);
+        }
+      }
+
+      /**
+       * This functions parses the huffman table and try to figure out if huffman
+       * optimizations are applied on the image or not. If segmentData has more bytes after
+       * parsing first HT, that means DHT segment has multiple huffman tables.
+       * Structure of DHT (Define Huffman Table) segment.
+       *   - HT information (1 byte): (bit 0 = LSB and bit 7 = MSB)
+       *     bit 3..0: index of HT (3..0, otherwise error)
+       *     bit 4   : type of HT, 0 = DC table, 1 = AC table
+       *     bit 7..5: not used, must be 0
+       *   - 16 bytes: number of symbols with codes of length 1..16, the sum of these
+       *     bytes is the total number of codes, which must be <= 256
+       *   - n bytes: table containing the symbols in order of increasing code length
+       *     (n = total number of codes)
+       *
+       * @param markerLength length of the DHT marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseHuffmanTables(int markerLength, byte[] segmentData)
+          throws ImageReadException, IOException {
+        InputStream is = new ByteArrayInputStream(segmentData);
+
+        int toBeProcessed = markerLength -2;
+        while (toBeProcessed > 1) {
+          // Reading the table info byte.
+          int tableInfo = binaryParser.readByte("Huffman Table Info", is,
+                                                "Not able to read Huffman Table Info");
+          toBeProcessed--;
+
+          // Reading the counts of symbols from length 1...16.
+          if (toBeProcessed < 16) {
+            return;
+          }
+          int numSymbols =0;
+          for (int i = 0; i < 16; i++) {
+            numSymbols += binaryParser.readByte("Num symbols", is,
+                                                "Not able to read num symbols");
+          }
+          toBeProcessed -= 16 + numSymbols;
+
+          // It is highly unlikely that a huffman optimized image has same number of
+          // symbols as the standard huffman table. So, if DC tables has less than 12 symbols
+          // (OR) an AC table has less than 162 symbols it is most likely optimized.
+          int tableType = (tableInfo>>4) & 1;
+          if ((tableType == 0 && numSymbols != MAX_DC_SYMBOLS) ||
+              (tableType == 1 && numSymbols != MAX_AC_SYMBOLS)) {
+            imageParams.setHuffmanOptimized(true);
+            return;
+          }
+        }
+      }
+    };
+
+    new JpegUtils().traverseJFIF(new ByteSourceInputStream(is, filename), visitor);
+    return imageParams;
+  }
+}
\ No newline at end of file

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java?rev=1049663&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java Wed Dec 15 18:38:10 2010
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Assert;
+
+import java.io.*;
+
+/**
+ * Tests for {@code JpegImageUtils}
+ */
+public class JpegImageUtilsTest extends Assert {
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  @Test
+  public void testGetJpegImageData_huffmanOptimized() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage420.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(true, imageParams.isHuffmanOptimized());
+  }
+
+  @Test
+  public void testGetJpegImageData_420Sampling() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage420.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(JpegImageUtils.SamplingModes.YUV420, imageParams.getSamplingMode());
+  }
+
+  @Test
+  public void testGetJpegImageData_444Sampling() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage444.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(JpegImageUtils.SamplingModes.YUV444, imageParams.getSamplingMode());
+    assertEquals(0.90F, imageParams.getChromaQualityFactor(), 0.01F);
+    assertEquals(0.90F, imageParams.getLumaQualityFactor(), 0.01F);
+    assertEquals(0.90F, imageParams.getApproxQualityFactor(), 0.01F);
+  }
+
+  @Test
+  public void testGetJpegImageData_notHuffmanOptimized() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(false, imageParams.isHuffmanOptimized());
+  }  
+}

Added: shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg?rev=1049663&view=auto
==============================================================================
Files shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg (added) and shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg Wed Dec 15 18:38:10 2010 differ

Added: shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg?rev=1049663&view=auto
==============================================================================
Files shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg (added) and shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg Wed Dec 15 18:38:10 2010 differ

Added: shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg?rev=1049663&view=auto
==============================================================================
Files shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg (added) and shindig/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg Wed Dec 15 18:38:10 2010 differ