| /* |
| * 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; |
| } |
| } |