001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.swt.internal.image;
011:
012: import java.io.*;
013: import org.eclipse.swt.*;
014: import org.eclipse.swt.graphics.*;
015: import org.eclipse.swt.internal.*;
016:
017: final class PNGFileFormat extends FileFormat {
018: static final int SIGNATURE_LENGTH = 8;
019: static final int PRIME = 65521;
020: PngIhdrChunk headerChunk;
021: PngPlteChunk paletteChunk;
022: ImageData imageData;
023: byte[] data;
024: byte[] alphaPalette;
025: byte headerByte1;
026: byte headerByte2;
027: int adler;
028:
029: /**
030: * Skip over signature data. This has already been
031: * verified in isFileFormat().
032: */
033: void readSignature() throws IOException {
034: byte[] signature = new byte[SIGNATURE_LENGTH];
035: inputStream.read(signature);
036: }
037:
038: /**
039: * Load the PNG image from the byte stream.
040: */
041: ImageData[] loadFromByteStream() {
042: try {
043: readSignature();
044: PngChunkReader chunkReader = new PngChunkReader(inputStream);
045: headerChunk = chunkReader.getIhdrChunk();
046: int width = headerChunk.getWidth(), height = headerChunk
047: .getHeight();
048: if (width <= 0 || height <= 0)
049: SWT.error(SWT.ERROR_INVALID_IMAGE);
050: int imageSize = getAlignedBytesPerRow() * height;
051: data = new byte[imageSize];
052: imageData = ImageData.internal_new(width, height,
053: headerChunk.getSwtBitsPerPixel(), new PaletteData(
054: 0, 0, 0), 4, data, 0, null, null, -1, -1,
055: SWT.IMAGE_PNG, 0, 0, 0, 0);
056:
057: if (headerChunk.usesDirectColor()) {
058: imageData.palette = headerChunk.getPaletteData();
059: }
060:
061: // Read and process chunks until the IEND chunk is encountered.
062: while (chunkReader.hasMoreChunks()) {
063: readNextChunk(chunkReader);
064: }
065:
066: return new ImageData[] { imageData };
067: } catch (IOException e) {
068: SWT.error(SWT.ERROR_INVALID_IMAGE);
069: return null;
070: }
071: }
072:
073: /**
074: * Read and handle the next chunk of data from the
075: * PNG file.
076: */
077: void readNextChunk(PngChunkReader chunkReader) throws IOException {
078: PngChunk chunk = chunkReader.readNextChunk();
079: switch (chunk.getChunkType()) {
080: case PngChunk.CHUNK_IEND:
081: break;
082: case PngChunk.CHUNK_PLTE:
083: if (!headerChunk.usesDirectColor()) {
084: paletteChunk = (PngPlteChunk) chunk;
085: imageData.palette = paletteChunk.getPaletteData();
086: }
087: break;
088: case PngChunk.CHUNK_tRNS:
089: PngTrnsChunk trnsChunk = (PngTrnsChunk) chunk;
090: if (trnsChunk.getTransparencyType(headerChunk) == PngTrnsChunk.TRANSPARENCY_TYPE_PIXEL) {
091: imageData.transparentPixel = trnsChunk
092: .getSwtTransparentPixel(headerChunk);
093: } else {
094: alphaPalette = trnsChunk.getAlphaValues(headerChunk,
095: paletteChunk);
096: int transparentCount = 0, transparentPixel = -1;
097: for (int i = 0; i < alphaPalette.length; i++) {
098: if ((alphaPalette[i] & 0xFF) != 255) {
099: transparentCount++;
100: transparentPixel = i;
101: }
102: }
103: if (transparentCount == 0) {
104: alphaPalette = null;
105: } else if (transparentCount == 1
106: && alphaPalette[transparentPixel] == 0) {
107: alphaPalette = null;
108: imageData.transparentPixel = transparentPixel;
109: }
110: }
111: break;
112: case PngChunk.CHUNK_IDAT:
113: if (chunkReader.readPixelData()) {
114: // All IDAT chunks in an image file must be
115: // sequential. If the pixel data has already
116: // been read and another IDAT block is encountered,
117: // then this is an invalid image.
118: SWT.error(SWT.ERROR_INVALID_IMAGE);
119: } else {
120: // Read in the pixel data for the image. This should
121: // go through all the image's IDAT chunks.
122: PngIdatChunk dataChunk = (PngIdatChunk) chunk;
123: readPixelData(dataChunk, chunkReader);
124: }
125: break;
126: default:
127: if (chunk.isCritical()) {
128: // All critical chunks must be supported.
129: SWT.error(SWT.ERROR_NOT_IMPLEMENTED);
130: }
131: }
132: }
133:
134: void unloadIntoByteStream(ImageLoader loader) {
135: PngEncoder encoder = new PngEncoder(loader);
136: encoder.encode(outputStream);
137: }
138:
139: boolean isFileFormat(LEDataInputStream stream) {
140: try {
141: byte[] signature = new byte[SIGNATURE_LENGTH];
142: stream.read(signature);
143: stream.unread(signature);
144: if ((signature[0] & 0xFF) != 137)
145: return false; //137
146: if ((signature[1] & 0xFF) != 80)
147: return false; //P
148: if ((signature[2] & 0xFF) != 78)
149: return false; //N
150: if ((signature[3] & 0xFF) != 71)
151: return false; //G
152: if ((signature[4] & 0xFF) != 13)
153: return false; //<RETURN>
154: if ((signature[5] & 0xFF) != 10)
155: return false; //<LINEFEED>
156: if ((signature[6] & 0xFF) != 26)
157: return false; //<CTRL/Z>
158: if ((signature[7] & 0xFF) != 10)
159: return false; //<LINEFEED>
160: return true;
161: } catch (Exception e) {
162: return false;
163: }
164: }
165:
166: /**
167: * SWT does not support 16-bit depths. If this image uses
168: * 16-bit depths, convert the data to an 8-bit depth.
169: */
170: byte[] validateBitDepth(byte[] data) {
171: if (headerChunk.getBitDepth() > 8) {
172: byte[] result = new byte[data.length / 2];
173: compress16BitDepthTo8BitDepth(data, 0, result, 0,
174: result.length);
175: return result;
176: } else {
177: return data;
178: }
179: }
180:
181: /**
182: * SWT does not support greyscale as a color type. For
183: * plain grayscale, we create a palette. For Grayscale
184: * with Alpha, however, we need to convert the pixels
185: * to use RGB values.
186: * Note: This method assumes that the bit depth of the
187: * data has already been restricted to 8 or less.
188: */
189: void setPixelData(byte[] data, ImageData imageData) {
190: switch (headerChunk.getColorType()) {
191: case PngIhdrChunk.COLOR_TYPE_GRAYSCALE_WITH_ALPHA: {
192: int width = imageData.width;
193: int height = imageData.height;
194: int destBytesPerLine = imageData.bytesPerLine;
195: /*
196: * If the image uses 16-bit depth, it is converted
197: * to an 8-bit depth image.
198: */
199: int srcBytesPerLine = getAlignedBytesPerRow();
200: if (headerChunk.getBitDepth() > 8)
201: srcBytesPerLine /= 2;
202:
203: byte[] rgbData = new byte[destBytesPerLine * height];
204: byte[] alphaData = new byte[width * height];
205: for (int y = 0; y < height; y++) {
206: int srcIndex = srcBytesPerLine * y;
207: int destIndex = destBytesPerLine * y;
208: int destAlphaIndex = width * y;
209: for (int x = 0; x < width; x++) {
210: byte grey = data[srcIndex];
211: byte alpha = data[srcIndex + 1];
212: rgbData[destIndex + 0] = grey;
213: rgbData[destIndex + 1] = grey;
214: rgbData[destIndex + 2] = grey;
215: alphaData[destAlphaIndex] = alpha;
216: srcIndex += 2;
217: destIndex += 3;
218: destAlphaIndex++;
219: }
220: }
221: imageData.data = rgbData;
222: imageData.alphaData = alphaData;
223: break;
224: }
225: case PngIhdrChunk.COLOR_TYPE_RGB_WITH_ALPHA: {
226: int width = imageData.width;
227: int height = imageData.height;
228: int destBytesPerLine = imageData.bytesPerLine;
229: int srcBytesPerLine = getAlignedBytesPerRow();
230: /*
231: * If the image uses 16-bit depth, it is converted
232: * to an 8-bit depth image.
233: */
234: if (headerChunk.getBitDepth() > 8)
235: srcBytesPerLine /= 2;
236:
237: byte[] rgbData = new byte[destBytesPerLine * height];
238: byte[] alphaData = new byte[width * height];
239: for (int y = 0; y < height; y++) {
240: int srcIndex = srcBytesPerLine * y;
241: int destIndex = destBytesPerLine * y;
242: int destAlphaIndex = width * y;
243: for (int x = 0; x < width; x++) {
244: rgbData[destIndex + 0] = data[srcIndex + 0];
245: rgbData[destIndex + 1] = data[srcIndex + 1];
246: rgbData[destIndex + 2] = data[srcIndex + 2];
247: alphaData[destAlphaIndex] = data[srcIndex + 3];
248: srcIndex += 4;
249: destIndex += 3;
250: destAlphaIndex++;
251: }
252: }
253: imageData.data = rgbData;
254: imageData.alphaData = alphaData;
255: break;
256: }
257: case PngIhdrChunk.COLOR_TYPE_RGB:
258: imageData.data = data;
259: break;
260: case PngIhdrChunk.COLOR_TYPE_PALETTE:
261: imageData.data = data;
262: if (alphaPalette != null) {
263: int size = imageData.width * imageData.height;
264: byte[] alphaData = new byte[size];
265: byte[] pixelData = new byte[size];
266: imageData.getPixels(0, 0, size, pixelData, 0);
267: for (int i = 0; i < pixelData.length; i++) {
268: alphaData[i] = alphaPalette[pixelData[i] & 0xFF];
269: }
270: imageData.alphaData = alphaData;
271: }
272: break;
273: default:
274: imageData.data = data;
275: break;
276: }
277: }
278:
279: /**
280: * PNG supports some color types and bit depths that are
281: * unsupported by SWT. If the image uses an unsupported
282: * color type (either of the gray scale types) or bit
283: * depth (16), convert the data to an SWT-supported
284: * format. Then assign the data into the ImageData given.
285: */
286: void setImageDataValues(byte[] data, ImageData imageData) {
287: byte[] result = validateBitDepth(data);
288: setPixelData(result, imageData);
289: }
290:
291: /**
292: * Read the image data from the data stream. This must handle
293: * decoding the data, filtering, and interlacing.
294: */
295: void readPixelData(PngIdatChunk chunk, PngChunkReader chunkReader)
296: throws IOException {
297: InputStream stream = new PngInputStream(chunk, chunkReader);
298: //TEMPORARY CODE
299: boolean use3_2 = System
300: .getProperty("org.eclipse.swt.internal.image.PNGFileFormat_3.2") != null;
301: InputStream inflaterStream = use3_2 ? null : Compatibility
302: .newInflaterInputStream(stream);
303: if (inflaterStream != null) {
304: stream = new BufferedInputStream(inflaterStream);
305: } else {
306: stream = new PngDecodingDataStream(stream);
307: }
308: int interlaceMethod = headerChunk.getInterlaceMethod();
309: if (interlaceMethod == PngIhdrChunk.INTERLACE_METHOD_NONE) {
310: readNonInterlacedImage(stream);
311: } else {
312: readInterlacedImage(stream);
313: }
314: /*
315: * InflaterInputStream does not consume all bytes in the stream
316: * when it is closed. This may leave unread IDAT chunks. The fix
317: * is to read all available bytes before closing it.
318: */
319: while (stream.available() > 0)
320: stream.read();
321: stream.close();
322: }
323:
324: /**
325: * Answer the number of bytes in a word-aligned row of pixel data.
326: */
327: int getAlignedBytesPerRow() {
328: return ((getBytesPerRow(headerChunk.getWidth()) + 3) / 4) * 4;
329: }
330:
331: /**
332: * Answer the number of bytes in each row of the image
333: * data. Each PNG row is byte-aligned, so images with bit
334: * depths less than a byte may have unused bits at the
335: * end of each row. The value of these bits is undefined.
336: */
337: int getBytesPerRow() {
338: return getBytesPerRow(headerChunk.getWidth());
339: }
340:
341: /**
342: * Answer the number of bytes needed to represent a pixel.
343: * This value depends on the image's color type and bit
344: * depth.
345: * Note that this method rounds up if an image's pixel size
346: * isn't byte-aligned.
347: */
348: int getBytesPerPixel() {
349: int bitsPerPixel = headerChunk.getBitsPerPixel();
350: return (bitsPerPixel + 7) / 8;
351: }
352:
353: /**
354: * Answer the number of bytes in a row of the given pixel
355: * width. Each row is byte-aligned, so images with bit
356: * depths less than a byte may have unused bits at the
357: * end of each row. The value of these bits is undefined.
358: */
359: int getBytesPerRow(int rowWidthInPixels) {
360: int bitsPerPixel = headerChunk.getBitsPerPixel();
361: int bitsPerRow = bitsPerPixel * rowWidthInPixels;
362: int bitsPerByte = 8;
363: return (bitsPerRow + (bitsPerByte - 1)) / bitsPerByte;
364: }
365:
366: /**
367: * 1. Read one of the seven frames of interlaced data.
368: * 2. Update the imageData.
369: * 3. Notify the image loader's listeners of the frame load.
370: */
371: void readInterlaceFrame(InputStream inputStream, int rowInterval,
372: int columnInterval, int startRow, int startColumn,
373: int frameCount) throws IOException {
374: int width = headerChunk.getWidth();
375: int alignedBytesPerRow = getAlignedBytesPerRow();
376: int height = headerChunk.getHeight();
377: if (startRow >= height || startColumn >= width)
378: return;
379:
380: int pixelsPerRow = (width - startColumn + columnInterval - 1)
381: / columnInterval;
382: int bytesPerRow = getBytesPerRow(pixelsPerRow);
383: byte[] row1 = new byte[bytesPerRow];
384: byte[] row2 = new byte[bytesPerRow];
385: byte[] currentRow = row1;
386: byte[] lastRow = row2;
387: for (int row = startRow; row < height; row += rowInterval) {
388: byte filterType = (byte) inputStream.read();
389: int read = 0;
390: while (read != bytesPerRow) {
391: read += inputStream.read(currentRow, read, bytesPerRow
392: - read);
393: }
394: filterRow(currentRow, lastRow, filterType);
395: if (headerChunk.getBitDepth() >= 8) {
396: int bytesPerPixel = getBytesPerPixel();
397: int dataOffset = (row * alignedBytesPerRow)
398: + (startColumn * bytesPerPixel);
399: for (int rowOffset = 0; rowOffset < currentRow.length; rowOffset += bytesPerPixel) {
400: for (int byteOffset = 0; byteOffset < bytesPerPixel; byteOffset++) {
401: data[dataOffset + byteOffset] = currentRow[rowOffset
402: + byteOffset];
403: }
404: dataOffset += (columnInterval * bytesPerPixel);
405: }
406: } else {
407: int bitsPerPixel = headerChunk.getBitDepth();
408: int pixelsPerByte = 8 / bitsPerPixel;
409: int column = startColumn;
410: int rowBase = row * alignedBytesPerRow;
411: int valueMask = 0;
412: for (int i = 0; i < bitsPerPixel; i++) {
413: valueMask <<= 1;
414: valueMask |= 1;
415: }
416: int maxShift = 8 - bitsPerPixel;
417: for (int byteOffset = 0; byteOffset < currentRow.length; byteOffset++) {
418: for (int bitOffset = maxShift; bitOffset >= 0; bitOffset -= bitsPerPixel) {
419: if (column < width) {
420: int dataOffset = rowBase
421: + (column * bitsPerPixel / 8);
422: int value = (currentRow[byteOffset] >> bitOffset)
423: & valueMask;
424: int dataShift = maxShift
425: - (bitsPerPixel * (column % pixelsPerByte));
426: data[dataOffset] |= value << dataShift;
427: }
428: column += columnInterval;
429: }
430: }
431: }
432: currentRow = (currentRow == row1) ? row2 : row1;
433: lastRow = (lastRow == row1) ? row2 : row1;
434: }
435: setImageDataValues(data, imageData);
436: fireInterlacedFrameEvent(frameCount);
437: }
438:
439: /**
440: * Read the pixel data for an interlaced image from the
441: * data stream.
442: */
443: void readInterlacedImage(InputStream inputStream)
444: throws IOException {
445: readInterlaceFrame(inputStream, 8, 8, 0, 0, 0);
446: readInterlaceFrame(inputStream, 8, 8, 0, 4, 1);
447: readInterlaceFrame(inputStream, 8, 4, 4, 0, 2);
448: readInterlaceFrame(inputStream, 4, 4, 0, 2, 3);
449: readInterlaceFrame(inputStream, 4, 2, 2, 0, 4);
450: readInterlaceFrame(inputStream, 2, 2, 0, 1, 5);
451: readInterlaceFrame(inputStream, 2, 1, 1, 0, 6);
452: }
453:
454: /**
455: * Fire an event to let listeners know that an interlaced
456: * frame has been loaded.
457: * finalFrame should be true if the image has finished
458: * loading, false if there are more frames to come.
459: */
460: void fireInterlacedFrameEvent(int frameCount) {
461: if (loader.hasListeners()) {
462: ImageData image = (ImageData) imageData.clone();
463: boolean finalFrame = frameCount == 6;
464: loader.notifyListeners(new ImageLoaderEvent(loader, image,
465: frameCount, finalFrame));
466: }
467: }
468:
469: /**
470: * Read the pixel data for a non-interlaced image from the
471: * data stream.
472: * Update the imageData to reflect the new data.
473: */
474: void readNonInterlacedImage(InputStream inputStream)
475: throws IOException {
476: int dataOffset = 0;
477: int alignedBytesPerRow = getAlignedBytesPerRow();
478: int bytesPerRow = getBytesPerRow();
479: byte[] row1 = new byte[bytesPerRow];
480: byte[] row2 = new byte[bytesPerRow];
481: byte[] currentRow = row1;
482: byte[] lastRow = row2;
483: int height = headerChunk.getHeight();
484: for (int row = 0; row < height; row++) {
485: byte filterType = (byte) inputStream.read();
486: int read = 0;
487: while (read != bytesPerRow) {
488: read += inputStream.read(currentRow, read, bytesPerRow
489: - read);
490: }
491: filterRow(currentRow, lastRow, filterType);
492: System.arraycopy(currentRow, 0, data, dataOffset,
493: bytesPerRow);
494: dataOffset += alignedBytesPerRow;
495: currentRow = (currentRow == row1) ? row2 : row1;
496: lastRow = (lastRow == row1) ? row2 : row1;
497: }
498: setImageDataValues(data, imageData);
499: }
500:
501: /**
502: * SWT does not support 16-bit depth color formats.
503: * Convert the 16-bit data to 8-bit data.
504: * The correct way to do this is to multiply each
505: * 16 bit value by the value:
506: * (2^8 - 1) / (2^16 - 1).
507: * The fast way to do this is just to drop the low
508: * byte of the 16-bit value.
509: */
510: static void compress16BitDepthTo8BitDepth(byte[] source,
511: int sourceOffset, byte[] destination,
512: int destinationOffset, int numberOfValues) {
513: //double multiplier = (Compatibility.pow2(8) - 1) / (Compatibility.pow2(16) - 1);
514: for (int i = 0; i < numberOfValues; i++) {
515: int sourceIndex = sourceOffset + (2 * i);
516: int destinationIndex = destinationOffset + i;
517: //int value = (source[sourceIndex] << 8) | source[sourceIndex + 1];
518: //byte compressedValue = (byte)(value * multiplier);
519: byte compressedValue = source[sourceIndex];
520: destination[destinationIndex] = compressedValue;
521: }
522: }
523:
524: /**
525: * SWT does not support 16-bit depth color formats.
526: * Convert the 16-bit data to 8-bit data.
527: * The correct way to do this is to multiply each
528: * 16 bit value by the value:
529: * (2^8 - 1) / (2^16 - 1).
530: * The fast way to do this is just to drop the low
531: * byte of the 16-bit value.
532: */
533: static int compress16BitDepthTo8BitDepth(int value) {
534: //double multiplier = (Compatibility.pow2(8) - 1) / (Compatibility.pow2(16) - 1);
535: //byte compressedValue = (byte)(value * multiplier);
536: return value >> 8;
537: }
538:
539: /**
540: * PNG supports four filtering types. These types are applied
541: * per row of image data. This method unfilters the given row
542: * based on the filterType.
543: */
544: void filterRow(byte[] row, byte[] previousRow, int filterType) {
545: int byteOffset = headerChunk.getFilterByteOffset();
546: switch (filterType) {
547: case PngIhdrChunk.FILTER_NONE:
548: break;
549: case PngIhdrChunk.FILTER_SUB:
550: for (int i = byteOffset; i < row.length; i++) {
551: int current = row[i] & 0xFF;
552: int left = row[i - byteOffset] & 0xFF;
553: row[i] = (byte) ((current + left) & 0xFF);
554: }
555: break;
556: case PngIhdrChunk.FILTER_UP:
557: for (int i = 0; i < row.length; i++) {
558: int current = row[i] & 0xFF;
559: int above = previousRow[i] & 0xFF;
560: row[i] = (byte) ((current + above) & 0xFF);
561: }
562: break;
563: case PngIhdrChunk.FILTER_AVERAGE:
564: for (int i = 0; i < row.length; i++) {
565: int left = (i < byteOffset) ? 0
566: : row[i - byteOffset] & 0xFF;
567: int above = previousRow[i] & 0xFF;
568: int current = row[i] & 0xFF;
569: row[i] = (byte) ((current + ((left + above) / 2)) & 0xFF);
570: }
571: break;
572: case PngIhdrChunk.FILTER_PAETH:
573: for (int i = 0; i < row.length; i++) {
574: int left = (i < byteOffset) ? 0
575: : row[i - byteOffset] & 0xFF;
576: int aboveLeft = (i < byteOffset) ? 0 : previousRow[i
577: - byteOffset] & 0xFF;
578: int above = previousRow[i] & 0xFF;
579:
580: int a = Math.abs(above - aboveLeft);
581: int b = Math.abs(left - aboveLeft);
582: int c = Math.abs(left - aboveLeft + above - aboveLeft);
583:
584: int preductor = 0;
585: if (a <= b && a <= c) {
586: preductor = left;
587: } else if (b <= c) {
588: preductor = above;
589: } else {
590: preductor = aboveLeft;
591: }
592:
593: int currentValue = row[i] & 0xFF;
594: row[i] = (byte) ((currentValue + preductor) & 0xFF);
595: }
596: break;
597: }
598: }
599:
600: }
|