blob: 71d4e1011cd7d43cf7463e990e89f3a53ceff644 [file] [log] [blame]
/*
* Licensed 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.
* under the License.
*/
package org.apache.commons.imaging.formats.jpeg.decoder;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.common.BinaryFileParser;
import org.apache.commons.imaging.common.bytesource.ByteSource;
import org.apache.commons.imaging.formats.jpeg.Block;
import org.apache.commons.imaging.formats.jpeg.JpegConstants;
import org.apache.commons.imaging.formats.jpeg.JpegUtils;
import org.apache.commons.imaging.formats.jpeg.ZigZag;
import org.apache.commons.imaging.formats.jpeg.segments.DhtSegment;
import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment;
import org.apache.commons.imaging.formats.jpeg.segments.SosSegment;
public class JpegDecoder extends BinaryFileParser implements JpegUtils.Visitor,
JpegConstants
{
/*
* JPEG is an advanced image format that takes
* significant computation to decode. Keep
* decoding fast:
* - Don't allocate memory inside loops,
* allocate it once and reuse.
* - Minimize calculations
* per pixel and per block (using lookup tables
* for YCbCr->RGB conversion doubled performance).
* - Math.round() is slow, use (int)(x+0.5f) instead
* for positive numbers.
*/
private DqtSegment.QuantizationTable[] quantizationTables = new DqtSegment.QuantizationTable[4];
private DhtSegment.HuffmanTable[] huffmanDCTables = new DhtSegment.HuffmanTable[4];
private DhtSegment.HuffmanTable[] huffmanACTables = new DhtSegment.HuffmanTable[4];
private SofnSegment sofnSegment;
private SosSegment sosSegment;
private float[][] scaledQuantizationTables = new float[4][];
private BufferedImage image = null;
private ImageReadException imageReadException = null;
private IOException ioException = null;
public boolean beginSOS()
{
return true;
}
public void visitSOS(int marker, byte markerBytes[],
byte imageData[])
{
ByteArrayInputStream is = new ByteArrayInputStream(imageData);
try
{
int segmentLength = read2Bytes("segmentLength", is,
"Not a Valid JPEG File");
byte[] sosSegmentBytes = readByteArray("SosSegment",
segmentLength - 2, is, "Not a Valid JPEG File");
sosSegment = new SosSegment(marker, sosSegmentBytes);
int hMax = 0;
int vMax = 0;
for (int i = 0; i < sofnSegment.numberOfComponents; i++)
{
hMax = Math.max(hMax, sofnSegment.components[i].horizontalSamplingFactor);
vMax = Math.max(vMax, sofnSegment.components[i].verticalSamplingFactor);
}
int hSize = 8*hMax;
int vSize = 8*vMax;
JpegInputStream bitInputStream = new JpegInputStream(is);
int xMCUs = (sofnSegment.width + hSize - 1) / hSize;
int yMCUs = (sofnSegment.height + vSize - 1) / vSize;
Block[] mcu = allocateMCUMemory();
Block[] scaledMCU = new Block[mcu.length];
for (int i = 0; i < scaledMCU.length; i++)
scaledMCU[i] = new Block(hSize, vSize);
int[] preds = new int[sofnSegment.numberOfComponents];
ColorModel colorModel;
WritableRaster raster;
if (sofnSegment.numberOfComponents == 3)
{
colorModel = new DirectColorModel(24,
0x00ff0000, 0x0000ff00, 0x000000ff);
raster = WritableRaster.createPackedRaster(DataBuffer.TYPE_INT,
sofnSegment.width, sofnSegment.height,
new int[]{0x00ff0000,0x0000ff00,0x000000ff}, null);
}
else if (sofnSegment.numberOfComponents == 1)
{
colorModel = new DirectColorModel(24,
0x00ff0000, 0x0000ff00, 0x000000ff);
raster = WritableRaster.createPackedRaster(DataBuffer.TYPE_INT,
sofnSegment.width, sofnSegment.height,
new int[]{0x00ff0000,0x0000ff00,0x000000ff}, null);
// FIXME: why do images come out too bright with CS_GRAY?
// colorModel = new ComponentColorModel(
// ColorSpace.getInstance(ColorSpace.CS_GRAY), false, true,
// Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
// raster = colorModel.createCompatibleWritableRaster(
// sofnSegment.width, sofnSegment.height);
}
else
throw new ImageReadException(sofnSegment.numberOfComponents +
" components are invalid or unsupported");
DataBuffer dataBuffer = raster.getDataBuffer();
for (int y1 = 0; y1 < vSize*yMCUs; y1 += vSize)
{
for (int x1 = 0; x1 < hSize*xMCUs; x1 += hSize)
{
readMCU(bitInputStream, preds, mcu);
rescaleMCU(mcu, hSize, vSize, scaledMCU);
int srcRowOffset = 0;
int dstRowOffset = y1*sofnSegment.width + x1;
for (int y2 = 0; y2 < vSize && y1 + y2 < sofnSegment.height; y2++)
{
for (int x2 = 0; x2 < hSize && x1 + x2 < sofnSegment.width; x2++)
{
if (scaledMCU.length == 3)
{
int Y = scaledMCU[0].samples[srcRowOffset + x2];
int Cb = scaledMCU[1].samples[srcRowOffset + x2];
int Cr = scaledMCU[2].samples[srcRowOffset + x2];
int rgb = YCbCrConverter.convertYCbCrToRGB(Y, Cb, Cr);
dataBuffer.setElem(dstRowOffset + x2, rgb);
}
else if (mcu.length == 1)
{
int Y = scaledMCU[0].samples[srcRowOffset + x2];
dataBuffer.setElem(dstRowOffset + x2,
(Y << 16) | (Y << 8) | Y);
}
else
throw new ImageReadException("Unsupported JPEG with " +
mcu.length + " components");
}
srcRowOffset += hSize;
dstRowOffset += sofnSegment.width;
}
}
}
image = new BufferedImage(colorModel, raster,
colorModel.isAlphaPremultiplied(), new Properties());
//byte[] remainder = super.getStreamBytes(is);
//for (int i = 0; i < remainder.length; i++)
//{
// System.out.println("" + i + " = " + Integer.toHexString(remainder[i]));
//}
}
catch (ImageReadException imageReadEx)
{
imageReadException = imageReadEx;
}
catch (IOException ioEx)
{
ioException = ioEx;
}
catch (RuntimeException ex)
{
// Corrupt images can throw NPE and IOOBE
imageReadException = new ImageReadException("Error parsing JPEG", ex);
}
}
public boolean visitSegment(int marker, byte[] markerBytes,
int segmentLength, byte[] segmentLengthBytes,
byte[] segmentData) throws ImageReadException, IOException
{
final int[] sofnSegments = {
SOF0Marker,
SOF1Marker, SOF2Marker, SOF3Marker, SOF5Marker, SOF6Marker,
SOF7Marker, SOF9Marker, SOF10Marker, SOF11Marker, SOF13Marker,
SOF14Marker, SOF15Marker,
};
if (Arrays.binarySearch(sofnSegments, marker) >= 0)
{
if (marker != SOF0Marker)
throw new ImageReadException("Only sequential, baseline JPEGs " +
"are supported at the moment");
sofnSegment = new SofnSegment(marker, segmentData);
}
else if (marker == DQTMarker)
{
DqtSegment dqtSegment = new DqtSegment(marker, segmentData);
for (int i = 0; i < dqtSegment.quantizationTables.size(); i++)
{
DqtSegment.QuantizationTable table = dqtSegment.quantizationTables.get(i);
if (0 > table.destinationIdentifier ||
table.destinationIdentifier >= quantizationTables.length)
throw new ImageReadException("Invalid quantization table identifier " +
table.destinationIdentifier);
quantizationTables[table.destinationIdentifier] = table;
int[] quantizationMatrixInt = new int[64];
ZigZag.zigZagToBlock(table.elements, quantizationMatrixInt);
float[] quantizationMatrixFloat = new float[64];
for (int j = 0; j < 64; j++)
quantizationMatrixFloat[j] = quantizationMatrixInt[j];
Dct.scaleDequantizationMatrix(quantizationMatrixFloat);
scaledQuantizationTables[table.destinationIdentifier] =
quantizationMatrixFloat;
}
}
else if (marker == DHTMarker)
{
DhtSegment dhtSegment = new DhtSegment(marker, segmentData);
for (int i = 0; i < dhtSegment.huffmanTables.size(); i++)
{
DhtSegment.HuffmanTable table = dhtSegment.huffmanTables.get(i);
DhtSegment.HuffmanTable[] tables;
if (table.tableClass == 0)
tables = huffmanDCTables;
else if (table.tableClass == 1)
tables = huffmanACTables;
else
throw new ImageReadException("Invalid huffman table class " +
table.tableClass);
if (0 > table.destinationIdentifier ||
table.destinationIdentifier >= tables.length)
throw new ImageReadException("Invalid huffman table identifier " +
table.destinationIdentifier);
tables[table.destinationIdentifier] = table;
}
}
return true;
}
private void rescaleMCU(Block[] dataUnits, int hSize, int vSize, Block[] ret)
{
for (int i = 0; i < dataUnits.length; i++)
{
Block block = dataUnits[i];
if (block.width == hSize && block.height == vSize)
System.arraycopy(block.samples, 0, ret[i].samples, 0, hSize*vSize);
else
{
int hScale = hSize / block.width;
int vScale = vSize / block.height;
if (hScale == 2 && vScale == 2)
{
int srcRowOffset = 0;
int dstRowOffset = 0;
for (int y = 0; y < block.height; y++)
{
for (int x = 0; x < hSize; x++)
{
int sample = block.samples[srcRowOffset + (x >> 1)];
ret[i].samples[dstRowOffset + x] = sample;
ret[i].samples[dstRowOffset + hSize + x] = sample;
}
srcRowOffset += block.width;
dstRowOffset += 2*hSize;
}
}
else
{
// FIXME: optimize
int dstRowOffset = 0;
for (int y = 0; y < vSize; y++)
{
for (int x = 0; x < hSize; x++)
{
ret[i].samples[dstRowOffset + x] =
block.samples[(y/vScale)*block.width + (x/hScale)];
}
dstRowOffset += hSize;
}
}
}
}
}
private Block[] allocateMCUMemory() throws ImageReadException
{
Block[] mcu = new Block[sosSegment.numberOfComponents];
for (int i = 0; i < sosSegment.numberOfComponents; i++)
{
SosSegment.Component scanComponent = sosSegment.components[i];
SofnSegment.Component frameComponent = null;
for (int j = 0; j < sofnSegment.numberOfComponents; j++)
{
if (sofnSegment.components[j].componentIdentifier ==
scanComponent.scanComponentSelector)
{
frameComponent = sofnSegment.components[j];
break;
}
}
if (frameComponent == null)
throw new ImageReadException("Invalid component");
Block fullBlock = new Block(
8*frameComponent.horizontalSamplingFactor,
8*frameComponent.verticalSamplingFactor);
mcu[i] = fullBlock;
}
return mcu;
}
private int[] zz = new int[64];
private int[] blockInt = new int[64];
private float[] block = new float[64];
private void readMCU(JpegInputStream is, int[] preds, Block[] mcu)
throws IOException, ImageReadException
{
for (int i = 0; i < sosSegment.numberOfComponents; i++)
{
SosSegment.Component scanComponent = sosSegment.components[i];
SofnSegment.Component frameComponent = null;
for (int j = 0; j < sofnSegment.numberOfComponents; j++)
{
if (sofnSegment.components[j].componentIdentifier ==
scanComponent.scanComponentSelector)
{
frameComponent = sofnSegment.components[j];
break;
}
}
if (frameComponent == null)
throw new ImageReadException("Invalid component");
Block fullBlock = mcu[i];
for (int y = 0; y < frameComponent.verticalSamplingFactor; y++)
{
for (int x = 0; x < frameComponent.horizontalSamplingFactor; x++)
{
Arrays.fill(zz, 0);
// page 104 of T.81
int t = decode(is,
huffmanDCTables[scanComponent.dcCodingTableSelector]);
int diff = receive(t, is);
diff = extend(diff, t);
zz[0] = preds[i] + diff;
preds[i] = zz[0];
// "Decode_AC_coefficients", figure F.13, page 106 of T.81
int k = 1;
while (true)
{
int rs = decode(is,
huffmanACTables[scanComponent.acCodingTableSelector]);
int ssss = rs & 0xf;
int rrrr = rs >> 4;
int r = rrrr;
if (ssss == 0)
{
if (r == 15)
k += 16;
else
break;
}
else
{
k += r;
// "Decode_ZZ(k)", figure F.14, page 107 of T.81
zz[k] = receive(ssss, is);
zz[k] = extend(zz[k], ssss);
if (k == 63)
break;
else
k++;
}
}
final int shift = (1 << (sofnSegment.precision - 1));
final int max = (1 << sofnSegment.precision) - 1;
float[] scaledQuantizationTable =
scaledQuantizationTables[frameComponent.quantTabDestSelector];
ZigZag.zigZagToBlock(zz, blockInt);
for (int j = 0; j < 64; j++)
block[j] = blockInt[j] * scaledQuantizationTable[j];
Dct.inverseDCT8x8(block);
int dstRowOffset = 8*y*8*frameComponent.horizontalSamplingFactor +
8*x;
int srcNext = 0;
for (int yy = 0; yy < 8; yy++)
{
for (int xx = 0; xx < 8; xx++)
{
float sample = block[srcNext++];
sample += shift;
int result;
if (sample < 0)
result = 0;
else if (sample > max)
result = max;
else
result = fastRound(sample);
fullBlock.samples[dstRowOffset + xx] = result;
}
dstRowOffset += 8*frameComponent.horizontalSamplingFactor;
}
}
}
}
}
private static int fastRound(float x)
{
return (int) (x + 0.5f);
}
private int extend(int v, int t)
{
// "EXTEND", section F.2.2.1, figure F.12, page 105 of T.81
int vt = (1 << (t - 1));
while (v < vt)
{
vt = (-1 << t) + 1;
v += vt;
}
return v;
}
private int receive(int ssss, JpegInputStream is)
throws IOException, ImageReadException
{
// "RECEIVE", section F.2.2.4, figure F.17, page 110 of T.81
int i = 0;
int v = 0;
while (i != ssss)
{
i++;
v = (v << 1) + is.nextBit();
}
return v;
}
private int decode(JpegInputStream is, DhtSegment.HuffmanTable huffmanTable)
throws IOException, ImageReadException
{
// "DECODE", section F.2.2.3, figure F.16, page 109 of T.81
int i = 1;
int code = is.nextBit();
while (code > huffmanTable.maxCode[i])
{
i++;
code = (code << 1) | is.nextBit();
}
int j = huffmanTable.valPtr[i];
j += code - huffmanTable.minCode[i];
int value = huffmanTable.huffVal[j];
return value;
}
public BufferedImage decode(ByteSource byteSource)
throws IOException, ImageReadException
{
JpegUtils jpegUtils = new JpegUtils();
jpegUtils.traverseJFIF(byteSource, this);
if (imageReadException != null)
throw imageReadException;
if (ioException != null)
throw ioException;
return image;
}
}