    001/*
    002 *  Licensed under the Apache License, Version 2.0 (the "License");
    003 *  you may not use this file except in compliance with the License.
    004 *  You may obtain a copy of the License at
    005 *
    006 *       http://www.apache.org/licenses/LICENSE-2.0
    007 *
    008 *  Unless required by applicable law or agreed to in writing, software
    009 *  distributed under the License is distributed on an "AS IS" BASIS,
    010 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    011 *  See the License for the specific language governing permissions and
    012 *  limitations under the License.
    013 *  under the License.
    014 */
    015
    016package org.apache.commons.imaging.formats.jpeg.decoder;
    017
    018import java.awt.image.BufferedImage;
    019import java.awt.image.ColorModel;
    020import java.awt.image.DataBuffer;
    021import java.awt.image.DirectColorModel;
    022import java.awt.image.Raster;
    023import java.awt.image.WritableRaster;
    024import java.io.ByteArrayInputStream;
    025import java.io.IOException;
    026import java.util.ArrayList;
    027import java.util.Arrays;
    028import java.util.List;
    029import java.util.Properties;
    030
    031import org.apache.commons.imaging.ImageReadException;
    032import org.apache.commons.imaging.color.ColorConversions;
    033import org.apache.commons.imaging.common.BinaryFileParser;
    034import org.apache.commons.imaging.common.bytesource.ByteSource;
    035import org.apache.commons.imaging.formats.jpeg.JpegConstants;
    036import org.apache.commons.imaging.formats.jpeg.JpegUtils;
    037import org.apache.commons.imaging.formats.jpeg.segments.DhtSegment;
    038import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
    039import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment;
    040import org.apache.commons.imaging.formats.jpeg.segments.SosSegment;
    041
    042import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
    043import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
    044
    045public class JpegDecoder extends BinaryFileParser implements JpegUtils.Visitor {
    046    /*
    047     * JPEG is an advanced image format that takes significant computation to
    048     * decode. Keep decoding fast: - Don't allocate memory inside loops,
    049     * allocate it once and reuse. - Minimize calculations per pixel and per
    050     * block (using lookup tables for YCbCr->RGB conversion doubled
    051     * performance). - Math.round() is slow, use (int)(x+0.5f) instead for
    052     * positive numbers.
    053     */
    054
    055    private final DqtSegment.QuantizationTable[] quantizationTables = new DqtSegment.QuantizationTable[4];
    056    private final DhtSegment.HuffmanTable[] huffmanDCTables = new DhtSegment.HuffmanTable[4];
    057    private final DhtSegment.HuffmanTable[] huffmanACTables = new DhtSegment.HuffmanTable[4];
    058    private SofnSegment sofnSegment;
    059    private SosSegment sosSegment;
    060    private final float[][] scaledQuantizationTables = new float[4][];
    061    private BufferedImage image;
    062    private ImageReadException imageReadException;
    063    private IOException ioException;
    064    private final int[] zz = new int[64];
    065    private final int[] blockInt = new int[64];
    066    private final float[] block = new float[64];
    067
    068    @Override
    069    public boolean beginSOS() {
    070        return true;
    071    }
    072
    073    @Override
    074    public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) {
    075        final ByteArrayInputStream is = new ByteArrayInputStream(imageData);
    076        try {
    077            // read the scan header
    078            final int segmentLength = read2Bytes("segmentLength", is,"Not a Valid JPEG File", getByteOrder());
    079            final byte[] sosSegmentBytes = readBytes("SosSegment", is, segmentLength - 2, "Not a Valid JPEG File");
    080            sosSegment = new SosSegment(marker, sosSegmentBytes);
    081            // read the payload of the scan, this is the remainder of image data after the header
    082            // the payload contains the entropy-encoded segments (or ECS) divided by RST markers
    083            // or only one ECS if the entropy-encoded data is not divided by RST markers
    084            // length of payload = length of image data - length of data already read
    085            final int[] scanPayload = new int[imageData.length - segmentLength];
    086            int payloadReadCount = 0;
    087            while (payloadReadCount < scanPayload.length) {
    088                scanPayload[payloadReadCount] = is.read();
    089                payloadReadCount++;
    090            }
    091
    092            int hMax = 0;
    093            int vMax = 0;
    094            for (int i = 0; i < sofnSegment.numberOfComponents; i++) {
    095                hMax = Math.max(hMax,
    096                        sofnSegment.getComponents(i).horizontalSamplingFactor);
    097                vMax = Math.max(vMax,
    098                        sofnSegment.getComponents(i).verticalSamplingFactor);
    099            }
    100            final int hSize = 8 * hMax;
    101            final int vSize = 8 * vMax;
    102
    103            final int xMCUs = (sofnSegment.width + hSize - 1) / hSize;
    104            final int yMCUs = (sofnSegment.height + vSize - 1) / vSize;
    105            final Block[] mcu = allocateMCUMemory();
    106            final Block[] scaledMCU = new Block[mcu.length];
    107            for (int i = 0; i < scaledMCU.length; i++) {
    108                scaledMCU[i] = new Block(hSize, vSize);
    109            }
    110            final int[] preds = new int[sofnSegment.numberOfComponents];
    111            ColorModel colorModel;
    112            WritableRaster raster;
    113            if (sofnSegment.numberOfComponents == 4) {
    114                colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff);
    115                int bandMasks[] = new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff };
    116                raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, sofnSegment.width, sofnSegment.height, bandMasks, null);
    117            } else if (sofnSegment.numberOfComponents == 3) {
    118                colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00,
    119                        0x000000ff);
    120                raster = Raster.createPackedRaster(DataBuffer.TYPE_INT,
    121                        sofnSegment.width, sofnSegment.height, new int[] {
    122                                0x00ff0000, 0x0000ff00, 0x000000ff }, null);
    123            } else if (sofnSegment.numberOfComponents == 1) {
    124                colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00,
    125                        0x000000ff);
    126                raster = Raster.createPackedRaster(DataBuffer.TYPE_INT,
    127                        sofnSegment.width, sofnSegment.height, new int[] {
    128                                0x00ff0000, 0x0000ff00, 0x000000ff }, null);
    129                // FIXME: why do images come out too bright with CS_GRAY?
    130                // colorModel = new ComponentColorModel(
    131                // ColorSpace.getInstance(ColorSpace.CS_GRAY), false, true,
    132                // Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
    133                // raster = colorModel.createCompatibleWritableRaster(
    134                // sofnSegment.width, sofnSegment.height);
    135            } else {
    136                throw new ImageReadException(sofnSegment.numberOfComponents
    137                        + " components are invalid or unsupported");
    138            }
    139            final DataBuffer dataBuffer = raster.getDataBuffer();
    140
    141            final JpegInputStream[] bitInputStreams = splitByRstMarkers(scanPayload);
    142            int bitInputStreamCount = 0;
    143            JpegInputStream bitInputStream = bitInputStreams[0];
    144
    145            for (int y1 = 0; y1 < vSize * yMCUs; y1 += vSize) {
    146                for (int x1 = 0; x1 < hSize * xMCUs; x1 += hSize) {
    147                    // Provide the next interval if an interval is read until it's end
    148                    // as long there are unread intervals available
    149                    if (!bitInputStream.hasNext()) {
    150                        bitInputStreamCount++;
    151                        if (bitInputStreamCount < bitInputStreams.length) {
    152                            bitInputStream = bitInputStreams[bitInputStreamCount];
    153                        }
    154                    }
    155
    156                    readMCU(bitInputStream, preds, mcu);
    157                    rescaleMCU(mcu, hSize, vSize, scaledMCU);
    158                    int srcRowOffset = 0;
    159                    int dstRowOffset = y1 * sofnSegment.width + x1;
    160                    for (int y2 = 0; y2 < vSize && y1 + y2 < sofnSegment.height; y2++) {
    161                        for (int x2 = 0; x2 < hSize
    162                                && x1 + x2 < sofnSegment.width; x2++) {
    163                            if (scaledMCU.length == 4) {
    164                                final int C = scaledMCU[0].samples[srcRowOffset + x2];
    165                                final int M = scaledMCU[1].samples[srcRowOffset + x2];
    166                                final int Y = scaledMCU[2].samples[srcRowOffset + x2];
    167                                final int K = scaledMCU[3].samples[srcRowOffset + x2];
    168                                final int rgb = ColorConversions.convertCMYKtoRGB(C, M, Y, K);
    169                                dataBuffer.setElem(dstRowOffset + x2, rgb);
    170                            } else if (scaledMCU.length == 3) {
    171                                final int Y = scaledMCU[0].samples[srcRowOffset + x2];
    172                                final int Cb = scaledMCU[1].samples[srcRowOffset + x2];
    173                                final int Cr = scaledMCU[2].samples[srcRowOffset + x2];
    174                                final int rgb = YCbCrConverter.convertYCbCrToRGB(Y,
    175                                        Cb, Cr);
    176                                dataBuffer.setElem(dstRowOffset + x2, rgb);
    177                            } else if (mcu.length == 1) {
    178                                final int Y = scaledMCU[0].samples[srcRowOffset + x2];
    179                                dataBuffer.setElem(dstRowOffset + x2, (Y << 16)
    180                                        | (Y << 8) | Y);
    181                            } else {
    182                                throw new ImageReadException(
    183                                        "Unsupported JPEG with " + mcu.length
    184                                                + " components");
    185                            }
    186                        }
    187                        srcRowOffset += hSize;
    188                        dstRowOffset += sofnSegment.width;
    189                    }
    190                }
    191            }
    192            image = new BufferedImage(colorModel, raster,
    193                    colorModel.isAlphaPremultiplied(), new Properties());
    194            // byte[] remainder = super.getStreamBytes(is);
    195            // for (int i = 0; i < remainder.length; i++)
    196            // {
    197            // System.out.println("" + i + " = " +
    198            // Integer.toHexString(remainder[i]));
    199            // }
    200        } catch (final ImageReadException imageReadEx) {
    201            imageReadException = imageReadEx;
    202        } catch (final IOException ioEx) {
    203            ioException = ioEx;
    204        } catch (final RuntimeException ex) {
    205            // Corrupt images can throw NPE and IOOBE
    206            imageReadException = new ImageReadException("Error parsing JPEG",ex);
    207        }
    208    }
    209
    210    @Override
    211    public boolean visitSegment(final int marker, final byte[] markerBytes,
    212            final int segmentLength, final byte[] segmentLengthBytes, final byte[] segmentData)
    213            throws ImageReadException, IOException {
    214        final int[] sofnSegments = {
    215                JpegConstants.SOF0_MARKER,
    216                JpegConstants.SOF1_MARKER,
    217                JpegConstants.SOF2_MARKER,
    218                JpegConstants.SOF3_MARKER,
    219                JpegConstants.SOF5_MARKER,
    220                JpegConstants.SOF6_MARKER,
    221                JpegConstants.SOF7_MARKER,
    222                JpegConstants.SOF9_MARKER,
    223                JpegConstants.SOF10_MARKER,
    224                JpegConstants.SOF11_MARKER,
    225                JpegConstants.SOF13_MARKER,
    226                JpegConstants.SOF14_MARKER,
    227                JpegConstants.SOF15_MARKER,
    228        };
    229
    230        if (Arrays.binarySearch(sofnSegments, marker) >= 0) {
    231            if (marker != JpegConstants.SOF0_MARKER) {
    232                throw new ImageReadException("Only sequential, baseline JPEGs "
    233                        + "are supported at the moment");
    234            }
    235            sofnSegment = new SofnSegment(marker, segmentData);
    236        } else if (marker == JpegConstants.DQT_MARKER) {
    237            final DqtSegment dqtSegment = new DqtSegment(marker, segmentData);
    238            for (int i = 0; i < dqtSegment.quantizationTables.size(); i++) {
    239                final DqtSegment.QuantizationTable table = dqtSegment.quantizationTables.get(i);
    240                if (0 > table.destinationIdentifier
    241                        || table.destinationIdentifier >= quantizationTables.length) {
    242                    throw new ImageReadException(
    243                            "Invalid quantization table identifier "
    244                                    + table.destinationIdentifier);
    245                }
    246                quantizationTables[table.destinationIdentifier] = table;
    247                final int[] quantizationMatrixInt = new int[64];
    248                ZigZag.zigZagToBlock(table.getElements(), quantizationMatrixInt);
    249                final float[] quantizationMatrixFloat = new float[64];
    250                for (int j = 0; j < 64; j++) {
    251                    quantizationMatrixFloat[j] = quantizationMatrixInt[j];
    252                }
    253                Dct.scaleDequantizationMatrix(quantizationMatrixFloat);
    254                scaledQuantizationTables[table.destinationIdentifier] = quantizationMatrixFloat;
    255            }
    256        } else if (marker == JpegConstants.DHT_MARKER) {
    257            final DhtSegment dhtSegment = new DhtSegment(marker, segmentData);
    258            for (int i = 0; i < dhtSegment.huffmanTables.size(); i++) {
    259                final DhtSegment.HuffmanTable table = dhtSegment.huffmanTables.get(i);
    260                DhtSegment.HuffmanTable[] tables;
    261                if (table.tableClass == 0) {
    262                    tables = huffmanDCTables;
    263                } else if (table.tableClass == 1) {
    264                    tables = huffmanACTables;
    265                } else {
    266                    throw new ImageReadException("Invalid huffman table class "
    267                            + table.tableClass);
    268                }
    269                if (0 > table.destinationIdentifier
    270                        || table.destinationIdentifier >= tables.length) {
    271                    throw new ImageReadException(
    272                            "Invalid huffman table identifier "
    273                                    + table.destinationIdentifier);
    274                }
    275                tables[table.destinationIdentifier] = table;
    276            }
    277        }
    278        return true;
    279    }
    280
    281    private void rescaleMCU(final Block[] dataUnits, final int hSize, final int vSize, final Block[] ret) {
    282        for (int i = 0; i < dataUnits.length; i++) {
    283            final Block dataUnit = dataUnits[i];
    284            if (dataUnit.width == hSize && dataUnit.height == vSize) {
    285                System.arraycopy(dataUnit.samples, 0, ret[i].samples, 0, hSize
    286                        * vSize);
    287            } else {
    288                final int hScale = hSize / dataUnit.width;
    289                final int vScale = vSize / dataUnit.height;
    290                if (hScale == 2 && vScale == 2) {
    291                    int srcRowOffset = 0;
    292                    int dstRowOffset = 0;
    293                    for (int y = 0; y < dataUnit.height; y++) {
    294                        for (int x = 0; x < hSize; x++) {
    295                            final int sample = dataUnit.samples[srcRowOffset + (x >> 1)];
    296                            ret[i].samples[dstRowOffset + x] = sample;
    297                            ret[i].samples[dstRowOffset + hSize + x] = sample;
    298                        }
    299                        srcRowOffset += dataUnit.width;
    300                        dstRowOffset += 2 * hSize;
    301                    }
    302                } else {
    303                    // FIXME: optimize
    304                    int dstRowOffset = 0;
    305                    for (int y = 0; y < vSize; y++) {
    306                        for (int x = 0; x < hSize; x++) {
    307                            ret[i].samples[dstRowOffset + x] = dataUnit.samples[(y / vScale)
    308                                    * dataUnit.width + (x / hScale)];
    309                        }
    310                        dstRowOffset += hSize;
    311                    }
    312                }
    313            }
    314        }
    315    }
    316
    317    private Block[] allocateMCUMemory() throws ImageReadException {
    318        final Block[] mcu = new Block[sosSegment.numberOfComponents];
    319        for (int i = 0; i < sosSegment.numberOfComponents; i++) {
    320            final SosSegment.Component scanComponent = sosSegment.getComponents(i);
    321            SofnSegment.Component frameComponent = null;
    322            for (int j = 0; j < sofnSegment.numberOfComponents; j++) {
    323                if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) {
    324                    frameComponent = sofnSegment.getComponents(j);
    325                    break;
    326                }
    327            }
    328            if (frameComponent == null) {
    329                throw new ImageReadException("Invalid component");
    330            }
    331            final Block fullBlock = new Block(
    332                    8 * frameComponent.horizontalSamplingFactor,
    333                    8 * frameComponent.verticalSamplingFactor);
    334            mcu[i] = fullBlock;
    335        }
    336        return mcu;
    337    }
    338
    339    private void readMCU(final JpegInputStream is, final int[] preds, final Block[] mcu)
    340            throws IOException, ImageReadException {
    341        for (int i = 0; i < sosSegment.numberOfComponents; i++) {
    342            final SosSegment.Component scanComponent = sosSegment.getComponents(i);
    343            SofnSegment.Component frameComponent = null;
    344            for (int j = 0; j < sofnSegment.numberOfComponents; j++) {
    345                if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) {
    346                    frameComponent = sofnSegment.getComponents(j);
    347                    break;
    348                }
    349            }
    350            if (frameComponent == null) {
    351                throw new ImageReadException("Invalid component");
    352            }
    353            final Block fullBlock = mcu[i];
    354            for (int y = 0; y < frameComponent.verticalSamplingFactor; y++) {
    355                for (int x = 0; x < frameComponent.horizontalSamplingFactor; x++) {
    356                    Arrays.fill(zz, 0);
    357                    // page 104 of T.81
    358                    final int t = decode(
    359                            is,
    360                            huffmanDCTables[scanComponent.dcCodingTableSelector]);
    361                    int diff = receive(t, is);
    362                    diff = extend(diff, t);
    363                    zz[0] = preds[i] + diff;
    364                    preds[i] = zz[0];
    365
    366                    // "Decode_AC_coefficients", figure F.13, page 106 of T.81
    367                    int k = 1;
    368                    while (true) {
    369                        final int rs = decode(
    370                                is,
    371                                huffmanACTables[scanComponent.acCodingTableSelector]);
    372                        final int ssss = rs & 0xf;
    373                        final int rrrr = rs >> 4;
    374                        final int r = rrrr;
    375
    376                        if (ssss == 0) {
    377                            if (r == 15) {
    378                                k += 16;
    379                            } else {
    380                                break;
    381                            }
    382                        } else {
    383                            k += r;
    384
    385                            // "Decode_ZZ(k)", figure F.14, page 107 of T.81
    386                            zz[k] = receive(ssss, is);
    387                            zz[k] = extend(zz[k], ssss);
    388
    389                            if (k == 63) {
    390                                break;
    391                            } else {
    392                                k++;
    393                            }
    394                        }
    395                    }
    396
    397                    final int shift = (1 << (sofnSegment.precision - 1));
    398                    final int max = (1 << sofnSegment.precision) - 1;
    399
    400                    final float[] scaledQuantizationTable = scaledQuantizationTables[frameComponent.quantTabDestSelector];
    401                    ZigZag.zigZagToBlock(zz, blockInt);
    402                    for (int j = 0; j < 64; j++) {
    403                        block[j] = blockInt[j] * scaledQuantizationTable[j];
    404                    }
    405                    Dct.inverseDCT8x8(block);
    406
    407                    int dstRowOffset = 8 * y * 8
    408                            * frameComponent.horizontalSamplingFactor + 8 * x;
    409                    int srcNext = 0;
    410                    for (int yy = 0; yy < 8; yy++) {
    411                        for (int xx = 0; xx < 8; xx++) {
    412                            float sample = block[srcNext++];
    413                            sample += shift;
    414                            int result;
    415                            if (sample < 0) {
    416                                result = 0;
    417                            } else if (sample > max) {
    418                                result = max;
    419                            } else {
    420                                result = fastRound(sample);
    421                            }
    422                            fullBlock.samples[dstRowOffset + xx] = result;
    423                        }
    424                        dstRowOffset += 8 * frameComponent.horizontalSamplingFactor;
    425                    }
    426                }
    427            }
    428        }
    429    }
    430
    431    /**
    432     * Returns an array of JpegInputStream where each field contains the JpegInputStream
    433     * for one interval.
    434     * @param scanPayload array to read intervals from
    435     * @return JpegInputStreams for all intervals, at least one stream is always provided
    436     */
    437    static JpegInputStream[] splitByRstMarkers(final int[] scanPayload) {
    438        final List<Integer> intervalStarts = getIntervalStartPositions(scanPayload);
    439        // get number of intervals in payload to init an array of appropriate length
    440        final int intervalCount = intervalStarts.size();
    441        JpegInputStream[] streams = new JpegInputStream[intervalCount];
    442        for (int i = 0; i < intervalCount; i++) {
    443            int from = intervalStarts.get(i);
    444            int to;
    445            if (i < intervalCount - 1) {
    446                // because each restart marker needs two bytes the end of
    447                // this interval is two bytes before the next interval starts
    448                to = intervalStarts.get(i + 1) - 2;
    449            } else { // the last interval ends with the array
    450                to = scanPayload.length;
    451            }
    452            int[] interval = Arrays.copyOfRange(scanPayload, from, to);
    453            streams[i] = new JpegInputStream(interval);
    454        }
    455        return streams;
    456    }
    457
    458    /**
    459     * Returns the positions of where each interval in the provided array starts. The number
    460     * of start positions is also the count of intervals while the number of restart markers
    461     * found is equal to the number of start positions minus one (because restart markers
    462     * are between intervals).
    463     *
    464     * @param scanPayload array to examine
    465     * @return the start positions
    466     */
    467    static List<Integer> getIntervalStartPositions(final int[] scanPayload) {
    468        final List<Integer> intervalStarts = new ArrayList<Integer>();
    469        intervalStarts.add(0);
    470        boolean foundFF = false;
    471        boolean foundD0toD7 = false;
    472        int pos = 0;
    473        while (pos < scanPayload.length) {
    474            if (foundFF) {
    475                // found 0xFF D0 .. 0xFF D7 => RST marker
    476                if (scanPayload[pos] >= (0xff & JpegConstants.RST0_MARKER) &&
    477                    scanPayload[pos] <= (0xff & JpegConstants.RST7_MARKER)) {
    478                    foundD0toD7 = true;
    479                } else { // found 0xFF followed by something else => no RST marker
    480                    foundFF = false;
    481                }
    482            }
    483
    484            if (scanPayload[pos] == 0xFF) {
    485                foundFF = true;
    486            }
    487
    488            // true if one of the RST markers was found
    489            if (foundFF && foundD0toD7) {
    490                // we need to add the position after the current position because
    491                // we had already read 0xFF and are now at 0xDn
    492                intervalStarts.add(pos + 1);
    493                foundFF = foundD0toD7 = false;
    494            }
    495            pos++;
    496        }
    497        return intervalStarts;
    498    }
    499
    500    private static int fastRound(final float x) {
    501        return (int) (x + 0.5f);
    502    }
    503
    504    private int extend(int v, final int t) {
    505        // "EXTEND", section F.2.2.1, figure F.12, page 105 of T.81
    506        int vt = (1 << (t - 1));
    507        if (v < vt) {
    508            vt = (-1 << t) + 1;
    509            v += vt;
    510        }
    511        return v;
    512    }
    513
    514    private int receive(final int ssss, final JpegInputStream is) throws IOException,
    515            ImageReadException {
    516        // "RECEIVE", section F.2.2.4, figure F.17, page 110 of T.81
    517        int i = 0;
    518        int v = 0;
    519        while (i != ssss) {
    520            i++;
    521            v = (v << 1) + is.nextBit();
    522        }
    523        return v;
    524    }
    525
    526    private int decode(final JpegInputStream is, final DhtSegment.HuffmanTable huffmanTable)
    527            throws IOException, ImageReadException {
    528        // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81
    529        int i = 1;
    530        int code = is.nextBit();
    531        while (code > huffmanTable.getMaxCode(i)) {
    532            i++;
    533            code = (code << 1) | is.nextBit();
    534        }
    535        int j = huffmanTable.getValPtr(i);
    536        j += code - huffmanTable.getMinCode(i);
    537        return huffmanTable.getHuffVal(j);
    538    }
    539
    540    public BufferedImage decode(final ByteSource byteSource) throws IOException,
    541            ImageReadException {
    542        final JpegUtils jpegUtils = new JpegUtils();
    543        jpegUtils.traverseJFIF(byteSource, this);
    544        if (imageReadException != null) {
    545            throw imageReadException;
    546        }
    547        if (ioException != null) {
    548            throw ioException;
    549        }
    550        return image;
    551    }
    552}