Package org.apache.shindig.gadgets.rewrite.image

Source Code of org.apache.shindig.gadgets.rewrite.image.JpegImageUtils$JpegImageParams

/*
* 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.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 = (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) {
        System.arraycopy(table, 0, tables[tableIndex], 0, 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;
  }
}
TOP

Related Classes of org.apache.shindig.gadgets.rewrite.image.JpegImageUtils$JpegImageParams

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.