001: /*
002: * $RCSfile: GIFImageDecoder.java,v $
003: *
004: * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
005: *
006: * Use is subject to license terms.
007: *
008: * $Revision: 1.2 $
009: * $Date: 2006/06/17 00:02:28 $
010: * $State: Exp $
011: */
012: package com.sun.media.jai.codecimpl;
013:
014: import java.awt.Point;
015: import java.awt.Rectangle;
016: import java.awt.RenderingHints;
017: import java.awt.image.BufferedImage;
018: import java.awt.image.ColorModel;
019: import java.awt.image.DataBuffer;
020: import java.awt.image.ImageProducer;
021: import java.awt.image.IndexColorModel;
022: import java.awt.image.PixelInterleavedSampleModel;
023: import java.awt.image.Raster;
024: import java.awt.image.RenderedImage;
025: import java.awt.image.SampleModel;
026: import java.awt.image.WritableRaster;
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.util.Arrays;
030: import java.util.HashMap;
031: import com.sun.media.jai.codec.ImageCodec;
032: import com.sun.media.jai.codec.ImageDecodeParam;
033: import com.sun.media.jai.codec.ImageDecoderImpl;
034: import com.sun.media.jai.codec.SeekableStream;
035: import com.sun.media.jai.codecimpl.ImagingListenerProxy;
036: import com.sun.media.jai.codecimpl.util.ImagingException;
037:
038: /**
039: * @since EA3
040: */
041: public class GIFImageDecoder extends ImageDecoderImpl {
042:
043: // The global color table.
044: private byte[] globalColorTable = null;
045:
046: // Whether the last page has been encountered.
047: private boolean maxPageFound = false;
048:
049: // The maximum allowable page for reading.
050: private int maxPage;
051:
052: // The previous page read.
053: private int prevPage = -1;
054:
055: // The previous page on which getTile() was invoked in this object.
056: private int prevSyncedPage = -1;
057:
058: // Map of Integer page numbers to RenderedImages.
059: private HashMap images = new HashMap();
060:
061: /**
062: * Read the overall stream header and return the global color map
063: * or <code>null</code>.
064: */
065: private static byte[] readHeader(SeekableStream input)
066: throws IOException {
067: byte[] globalColorTable = null;
068: try {
069: // Skip the version string and logical screen dimensions.
070: input.skipBytes(10);
071:
072: int packedFields = input.readUnsignedByte();
073: boolean globalColorTableFlag = (packedFields & 0x80) != 0;
074: int numGCTEntries = 1 << ((packedFields & 0x7) + 1);
075:
076: int backgroundColorIndex = input.readUnsignedByte();
077:
078: // Read the aspect ratio but ignore the returned value.
079: input.read();
080:
081: if (globalColorTableFlag) {
082: globalColorTable = new byte[3 * numGCTEntries];
083: input.readFully(globalColorTable);
084: } else {
085: globalColorTable = null;
086: }
087: } catch (IOException e) {
088: String message = JaiI18N.getString("GIFImageDecoder0");
089: ImagingListenerProxy.errorOccurred(message,
090: new ImagingException(message, e),
091: GIFImageDecoder.class, false);
092: // throw new IOException(JaiI18N.getString("GIFImageDecoder0"));
093: }
094:
095: return globalColorTable;
096: }
097:
098: public GIFImageDecoder(SeekableStream input, ImageDecodeParam param) {
099: super (input, param);
100: }
101:
102: public GIFImageDecoder(InputStream input, ImageDecodeParam param) {
103: super (input, param);
104: }
105:
106: public int getNumPages() throws IOException {
107: int page = prevPage + 1;
108:
109: while (!maxPageFound) {
110: try {
111: decodeAsRenderedImage(page++);
112: } catch (IOException e) {
113: // Ignore
114: }
115: }
116:
117: return maxPage + 1;
118: }
119:
120: public synchronized RenderedImage decodeAsRenderedImage(int page)
121: throws IOException {
122:
123: // Verify that the index is in range.
124: if (page < 0 || (maxPageFound && page > maxPage)) {
125: throw new IOException(JaiI18N.getString("GIFImageDecoder1"));
126: }
127:
128: // Attempt to get the image from the cache.
129: Integer pageKey = new Integer(page);
130: if (images.containsKey(pageKey)) {
131: return (RenderedImage) images.get(pageKey);
132: }
133:
134: // If the zeroth image, set the global color table.
135: if (prevPage == -1) {
136: try {
137: globalColorTable = readHeader(input);
138: } catch (IOException e) {
139: maxPageFound = true;
140: maxPage = -1;
141: throw e;
142: }
143: }
144:
145: // Force previous data to be read.
146: if (page > 0) {
147: for (int idx = prevSyncedPage + 1; idx < page; idx++) {
148: RenderedImage im = (RenderedImage) images
149: .get(new Integer(idx));
150: im.getTile(0, 0);
151: prevSyncedPage = idx;
152: }
153: }
154:
155: // Read as many images as possible.
156: RenderedImage image = null;
157: while (prevPage < page) {
158: int index = prevPage + 1;
159: RenderedImage ri = null;
160: try {
161: ri = new GIFImage(input, globalColorTable);
162: images.put(new Integer(index), ri);
163: if (index < page) {
164: ri.getTile(0, 0);
165: prevSyncedPage = index;
166: }
167: prevPage = index;
168: if (index == page) {
169: image = ri;
170: break;
171: }
172: } catch (IOException e) {
173: maxPageFound = true;
174: maxPage = prevPage;
175: String message = JaiI18N.getString("GIFImage3");
176: ImagingListenerProxy.errorOccurred(message,
177: new ImagingException(message, e), this , false);
178: // throw e;
179: }
180: }
181:
182: return image;
183: }
184: }
185:
186: /**
187: * @since 1.1.1
188: */
189: class GIFImage extends SimpleRenderedImage {
190: // Constants used to control interlacing.
191: private static final int[] INTERLACE_INCREMENT = { 8, 8, 4, 2, -1 };
192: private static final int[] INTERLACE_OFFSET = { 0, 4, 2, 1, -1 };
193:
194: // The source stream.
195: private SeekableStream input;
196:
197: // The interlacing flag.
198: private boolean interlaceFlag = false;
199:
200: // Variables used by LZW decoding
201: private byte[] block = new byte[255];
202: private int blockLength = 0;
203: private int bitPos = 0;
204: private int nextByte = 0;
205: private int initCodeSize;
206: private int clearCode;
207: private int eofCode;
208: private int bitsLeft;
209:
210: // 32-bit lookahead buffer
211: private int next32Bits = 0;
212:
213: // True if the end of the data blocks has been found,
214: // and we are simply draining the 32-bit buffer
215: private boolean lastBlockFound = false;
216:
217: // The current interlacing pass, starting with 0.
218: private int interlacePass = 0;
219:
220: // The image's tile.
221: private WritableRaster theTile = null;
222:
223: // Read blocks of 1-255 bytes, stop at a 0-length block
224: private void skipBlocks() throws IOException {
225: while (true) {
226: int length = input.readUnsignedByte();
227: if (length == 0) {
228: break;
229: }
230: input.skipBytes(length);
231: }
232: }
233:
234: /**
235: * Create a new <code>GIFImage</code>. The input stream must
236: * be positioned at the start of the image, i.e., not at the
237: * start of the overall stream.
238: *
239: * @param input the stream from which to read.
240: * @param globalColorTable the global colormap of <code>null</code>.
241: *
242: * @throws IOException.
243: */
244: GIFImage(SeekableStream input, byte[] globalColorTable)
245: throws IOException {
246: this .input = input;
247:
248: byte[] localColorTable = null;
249: boolean transparentColorFlag = false;
250: int transparentColorIndex = 0;
251:
252: // Read the image header initializing the local color table,
253: // if any, and the transparent index, if any.
254:
255: try {
256: long startPosition = input.getFilePointer();
257: while (true) {
258: int blockType = input.readUnsignedByte();
259: if (blockType == 0x2c) { // Image Descriptor
260: // Skip image top and left position.
261: input.skipBytes(4);
262:
263: width = input.readUnsignedShortLE();
264: height = input.readUnsignedShortLE();
265:
266: int idPackedFields = input.readUnsignedByte();
267: boolean localColorTableFlag = (idPackedFields & 0x80) != 0;
268: interlaceFlag = (idPackedFields & 0x40) != 0;
269: int numLCTEntries = 1 << ((idPackedFields & 0x7) + 1);
270:
271: if (localColorTableFlag) {
272: // Read color table if any
273: localColorTable = new byte[3 * numLCTEntries];
274: input.readFully(localColorTable);
275: } else {
276: localColorTable = null;
277: }
278:
279: // Now positioned at start of LZW-compressed pixels
280: break;
281: } else if (blockType == 0x21) { // Extension block
282: int label = input.readUnsignedByte();
283:
284: if (label == 0xf9) { // Graphics Control Extension
285: input.read(); // extension length
286: int gcePackedFields = input.readUnsignedByte();
287: transparentColorFlag = (gcePackedFields & 0x1) != 0;
288:
289: input.skipBytes(2); // delay time
290:
291: transparentColorIndex = input
292: .readUnsignedByte();
293:
294: input.read(); // terminator
295: } else if (label == 0x1) { // Plain text extension
296: // Skip content.
297: input.skipBytes(13);
298: // Read but do not save content.
299: skipBlocks();
300: } else if (label == 0xfe) { // Comment extension
301: // Read but do not save content.
302: skipBlocks();
303: } else if (label == 0xff) { // Application extension
304: // Skip content.
305: input.skipBytes(12);
306: // Read but do not save content.
307: skipBlocks();
308: } else {
309: // Skip over unknown extension blocks
310: int length = 0;
311: do {
312: length = input.readUnsignedByte();
313: input.skipBytes(length);
314: } while (length > 0);
315: }
316: } else {
317: throw new IOException(JaiI18N
318: .getString("GIFImage0")
319: + " " + blockType + "!");
320: }
321: }
322: } catch (IOException ioe) {
323: throw new IOException(JaiI18N.getString("GIFImage1"));
324: }
325:
326: // Set the image layout from the header information.
327:
328: // Set the image and tile grid origin to (0, 0).
329: minX = minY = tileGridXOffset = tileGridYOffset = 0;
330:
331: // Force the image to have a single tile.
332: tileWidth = width;
333: tileHeight = height;
334:
335: byte[] colorTable;
336: if (localColorTable != null) {
337: colorTable = localColorTable;
338: } else {
339: colorTable = globalColorTable;
340: }
341:
342: // Normalize color table length to 2^1, 2^2, 2^4, or 2^8
343: int length = colorTable.length / 3;
344: int bits;
345: if (length == 2) {
346: bits = 1;
347: } else if (length == 4) {
348: bits = 2;
349: } else if (length == 8 || length == 16) {
350: // Bump from 3 to 4 bits
351: bits = 4;
352: } else {
353: // Bump to 8 bits
354: bits = 8;
355: }
356: int lutLength = 1 << bits;
357: byte[] r = new byte[lutLength];
358: byte[] g = new byte[lutLength];
359: byte[] b = new byte[lutLength];
360:
361: // Entries from length + 1 to lutLength - 1 will be 0
362: int rgbIndex = 0;
363: for (int i = 0; i < length; i++) {
364: r[i] = colorTable[rgbIndex++];
365: g[i] = colorTable[rgbIndex++];
366: b[i] = colorTable[rgbIndex++];
367: }
368:
369: int[] bitsPerSample = new int[1];
370: bitsPerSample[0] = bits;
371:
372: sampleModel = new PixelInterleavedSampleModel(
373: DataBuffer.TYPE_BYTE, width, height, 1, width,
374: new int[] { 0 });
375:
376: if (!transparentColorFlag) {
377: if (ImageCodec.isIndicesForGrayscale(r, g, b))
378: colorModel = ImageCodec
379: .createComponentColorModel(sampleModel);
380: else
381: colorModel = new IndexColorModel(bits, r.length, r, g,
382: b);
383: } else {
384: colorModel = new IndexColorModel(bits, r.length, r, g, b,
385: transparentColorIndex);
386: }
387: }
388:
389: // BEGIN LZW CODE
390:
391: private void initNext32Bits() {
392: next32Bits = block[0] & 0xff;
393: next32Bits |= (block[1] & 0xff) << 8;
394: next32Bits |= (block[2] & 0xff) << 16;
395: next32Bits |= block[3] << 24;
396: nextByte = 4;
397: }
398:
399: // Load a block (1-255 bytes) at a time, and maintain
400: // a 32-bit lookahead buffer that is filled from the left
401: // and extracted from the right.
402: private int getCode(int codeSize, int codeMask) throws IOException {
403: //if (bitPos + codeSize > 32) {
404: if (bitsLeft <= 0) {
405: return eofCode; // No more data available
406: }
407:
408: int code = (next32Bits >> bitPos) & codeMask;
409: bitPos += codeSize;
410: bitsLeft -= codeSize;
411:
412: // Shift in a byte of new data at a time
413: while (bitPos >= 8 && !lastBlockFound) {
414: next32Bits >>>= 8;
415: bitPos -= 8;
416:
417: // Check if current block is out of bytes
418: if (nextByte >= blockLength) {
419: // Get next block size
420: blockLength = input.readUnsignedByte();
421: if (blockLength == 0) {
422: lastBlockFound = true;
423: if (bitsLeft < 0)
424: return eofCode;
425: else
426: return code;
427: } else {
428: int left = blockLength;
429: int off = 0;
430: while (left > 0) {
431: int nbytes = input.read(block, off, left);
432: off += nbytes;
433: left -= nbytes;
434: }
435:
436: bitsLeft += blockLength << 3;
437: nextByte = 0;
438: }
439: }
440:
441: next32Bits |= block[nextByte++] << 24;
442: }
443:
444: return code;
445: }
446:
447: private void initializeStringTable(int[] prefix, byte[] suffix,
448: byte[] initial, int[] length) {
449: int numEntries = 1 << initCodeSize;
450: for (int i = 0; i < numEntries; i++) {
451: prefix[i] = -1;
452: suffix[i] = (byte) i;
453: initial[i] = (byte) i;
454: length[i] = 1;
455: }
456:
457: // Fill in the entire table for robustness against
458: // out-of-sequence codes.
459: for (int i = numEntries; i < 4096; i++) {
460: prefix[i] = -1;
461: length[i] = 1;
462: }
463: }
464:
465: private Point outputPixels(byte[] string, int len, Point streamPos,
466: byte[] rowBuf) {
467: if (interlacePass < 0 || interlacePass > 3) {
468: return streamPos;
469: }
470:
471: for (int i = 0; i < len; i++) {
472: if (streamPos.x >= minX) {
473: rowBuf[streamPos.x - minX] = string[i];
474: }
475:
476: // Process end-of-row
477: ++streamPos.x;
478: if (streamPos.x == width) {
479: theTile.setDataElements(minX, streamPos.y, width, 1,
480: rowBuf);
481:
482: streamPos.x = 0;
483: if (interlaceFlag) {
484: streamPos.y += INTERLACE_INCREMENT[interlacePass];
485: if (streamPos.y >= height) {
486: ++interlacePass;
487: if (interlacePass > 3) {
488: return streamPos;
489: }
490: streamPos.y = INTERLACE_OFFSET[interlacePass];
491: }
492: } else {
493: ++streamPos.y;
494: }
495: }
496: }
497:
498: return streamPos;
499: }
500:
501: // END LZW CODE
502:
503: public synchronized Raster getTile(int tileX, int tileY) {
504:
505: // Should be a unique tile.
506: if (tileX != 0 || tileY != 0) {
507: throw new IllegalArgumentException(JaiI18N
508: .getString("GIFImage2"));
509: }
510:
511: // Return the tile if it's already computed.
512: if (theTile != null) {
513: return theTile;
514: }
515:
516: // Initialize the destination image
517: theTile = WritableRaster.createWritableRaster(sampleModel,
518: sampleModel.createDataBuffer(), null);
519:
520: // Position in stream coordinates.
521: Point streamPos = new Point(0, 0);
522:
523: // Allocate a row of memory.
524: byte[] rowBuf = new byte[width];
525:
526: try {
527: // Read and decode the image data, fill in theTile.
528: this .initCodeSize = input.readUnsignedByte();
529:
530: // Read first data block
531: this .blockLength = input.readUnsignedByte();
532: int left = blockLength;
533: int off = 0;
534: while (left > 0) {
535: int nbytes = input.read(block, off, left);
536: left -= nbytes;
537: off += nbytes;
538: }
539:
540: this .bitPos = 0;
541: this .nextByte = 0;
542: this .lastBlockFound = false;
543: this .bitsLeft = this .blockLength << 3;
544:
545: // Init 32-bit buffer
546: initNext32Bits();
547:
548: this .clearCode = 1 << initCodeSize;
549: this .eofCode = clearCode + 1;
550:
551: int code, oldCode = 0;
552:
553: int[] prefix = new int[4096];
554: byte[] suffix = new byte[4096];
555: byte[] initial = new byte[4096];
556: int[] length = new int[4096];
557: byte[] string = new byte[4096];
558:
559: initializeStringTable(prefix, suffix, initial, length);
560: int tableIndex = (1 << initCodeSize) + 2;
561: int codeSize = initCodeSize + 1;
562: int codeMask = (1 << codeSize) - 1;
563:
564: while (true) {
565: code = getCode(codeSize, codeMask);
566:
567: if (code == clearCode) {
568: initializeStringTable(prefix, suffix, initial,
569: length);
570: tableIndex = (1 << initCodeSize) + 2;
571: codeSize = initCodeSize + 1;
572: codeMask = (1 << codeSize) - 1;
573: code = getCode(codeSize, codeMask);
574: if (code == eofCode) {
575: return theTile;
576: }
577: } else if (code == eofCode) {
578: return theTile;
579: } else {
580: int newSuffixIndex;
581: if (code < tableIndex) {
582: newSuffixIndex = code;
583: } else { // code == tableIndex
584: newSuffixIndex = oldCode;
585: }
586:
587: int ti = tableIndex;
588: int oc = oldCode;
589:
590: prefix[ti] = oc;
591: suffix[ti] = initial[newSuffixIndex];
592: initial[ti] = initial[oc];
593: length[ti] = length[oc] + 1;
594:
595: ++tableIndex;
596: if ((tableIndex == (1 << codeSize))
597: && (tableIndex < 4096)) {
598: ++codeSize;
599: codeMask = (1 << codeSize) - 1;
600: }
601: }
602:
603: // Reverse code
604: int c = code;
605: int len = length[c];
606: for (int i = len - 1; i >= 0; i--) {
607: string[i] = suffix[c];
608: c = prefix[c];
609: }
610:
611: outputPixels(string, len, streamPos, rowBuf);
612: oldCode = code;
613: }
614: } catch (IOException e) {
615: String message = JaiI18N.getString("GIFImage3");
616: ImagingListenerProxy.errorOccurred(message,
617: new ImagingException(message, e), this , false);
618: // throw new RuntimeException(JaiI18N.getString("GIFImage3"));
619: } finally {
620: return theTile;
621: }
622: }
623:
624: public void dispose() {
625: theTile = null;
626: }
627: }
|