/* * BufferedImageDecoder.java * Transform * * Copyright (c) 2009-2010 Flagstone Software Ltd. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Flagstone Software Ltd. nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.flagstone.transform.util.image; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferUShort; import java.awt.image.IndexColorModel; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.Arrays; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import javax.imageio.ImageIO; import com.flagstone.transform.coder.Coder; import com.flagstone.transform.coder.LittleDecoder; import com.flagstone.transform.image.DefineImage; import com.flagstone.transform.image.DefineImage2; import com.flagstone.transform.image.ImageFormat; import com.flagstone.transform.image.ImageTag; /** * BufferedImageDecoder decodes BufferedImages so they can be used in a Flash * file. The class also provides a set of convenience methods for converting * Flash images definitions into BufferedImages allowing the images to easily * be extracted from a Flash movie. */ @SuppressWarnings("PMD.TooManyMethods") public final class BufferedImageDecoder implements ImageProvider, ImageDecoder { /** Message used to signal that the image cannot be decoded. */ private static final String BAD_FORMAT = "Unsupported format"; /** The number of bytes per pixel in a RGBA format image. */ private static final int BYTES_PER_PIXEL = 4; /** The alpha channel level for an opaque pixel. */ private static final int OPAQUE = -1; /** Position in 32-bit word of red channel. */ private static final int RED = 0; /** Position in 32-bit word of green channel. */ private static final int GREEN = 1; /** Position in 32-bit word of blue channel. */ private static final int BLUE = 2; /** Position in 32-bit word of alpha channel. */ private static final int ALPHA = 3; /** Mask applied to extract 8-bit values. */ private static final int MASK_8BIT = 0x00FF; /** * Number of bits to shift when aligning to the second byte in a 16-bit * or 32-bit word. */ private static final int ALIGN_BYTE2 = 8; /** * Number of bits to shift when aligning to the second byte in a 16-bit * or 32-bit word. */ private static final int ALIGN_BYTE3 = 16; /** * Number of bits to shift when aligning to the second byte in a 32-bit * word. */ private static final int ALIGN_BYTE4 = 24; /** * Value added to offsets to ensure image width is aligned on a 16-bit * boundary, 3 == 2 bytes + 1. */ private static final int WORD_ALIGN = 3; /** Size of each colour table entry or pixel in a true colour image. */ private static final int COLOUR_CHANNELS = 4; /** Size of each colour table entry or pixel in a RGB image. */ private static final int RGB_CHANNELS = 3; /** Size of a pixel in a RGB555 true colour image. */ private static final int RGB5_SIZE = 16; /** Size of a pixel in a RGB8 true colour image. */ private static final int RGB8_SIZE = 24; /** Shift used to align the RGB555 red channel to a 8-bit pixel. */ private static final int RGB5_MSB_MASK = 0x00F8; /** Shift used to align the RGB555 red channel to a 8-bit pixel. */ private static final int RGB5_RED_SHIFT = 7; /** Shift used to align the RGB555 green channel to a 8-bit pixel. */ private static final int RGB5_GREEN_SHIFT = 2; /** Shift used to align the RGB555 blue channel to a 8-bit pixel. */ private static final int RGB5_BLUE_SHIFT = 3; /** The format of the decoded image. */ private transient ImageFormat format; /** The width of the image in pixels. */ private transient int width; /** The height of the image in pixels. */ private transient int height; /** The colour table for indexed images. */ private transient byte[] table; /** The image data. */ private transient byte[] image; /** {@inheritDoc} */ public ImageDecoder newDecoder() { return new BufferedImageDecoder(); } /** {@inheritDoc} */ public void read(final File file) throws IOException, DataFormatException { read(new FileInputStream(file)); } /** {@inheritDoc} */ public void read(final URL url) throws IOException, DataFormatException { final URLConnection connection = url.openConnection(); final int fileSize = connection.getContentLength(); if (fileSize < 0) { throw new FileNotFoundException(url.getFile()); } read(url.openStream()); } /** {@inheritDoc} */ public void read(final InputStream stream) throws IOException, DataFormatException { read(ImageIO.read(stream)); } /** {@inheritDoc} */ public ImageTag defineImage(final int identifier) { ImageTag object = null; switch (format) { case IDX8: object = new DefineImage(identifier, width, height, table.length / COLOUR_CHANNELS, zip(merge(adjustScan(width, height, image), table))); break; case IDXA: object = new DefineImage2(identifier, width, height, table.length / COLOUR_CHANNELS, zip(mergeAlpha(adjustScan(width, height, image), table))); break; case RGB5: object = new DefineImage(identifier, width, height, zip(packColours(width, height, image)), RGB5_SIZE); break; case RGB8: orderAlpha(image); object = new DefineImage(identifier, width, height, zip(image), RGB8_SIZE); break; case RGBA: applyAlpha(image); object = new DefineImage2(identifier, width, height, zip(image)); break; default: throw new AssertionError(BAD_FORMAT); } return object; } /** * Create an image definition from a BufferedImage. * * @param identifier * the unique identifier that will be used to refer to the image * in the Flash file. * * @param obj * the BufferedImage containing the image. * * @return an image definition that can be added to a Movie. * * @throws IOException * if there is a problem extracting the image, from the * BufferedImage. * * @throws DataFormatException * if the BufferedImage contains a format that is not currently * supported. */ public ImageTag defineImage(final int identifier, final BufferedImage obj) throws IOException, DataFormatException { ImageTag object = null; final BufferedImageDecoder decoder = new BufferedImageDecoder(); decoder.read(obj); switch (format) { case IDX8: object = new DefineImage(identifier, width, height, table.length, zip(merge(adjustScan(width, height, image), table))); break; case IDXA: object = new DefineImage2(identifier, width, height, table.length, zip(mergeAlpha(adjustScan(width, height, image), table))); break; case RGB5: object = new DefineImage(identifier, width, height, zip(packColours(width, height, image)), RGB5_SIZE); break; case RGB8: orderAlpha(image); object = new DefineImage(identifier, width, height, zip(image), RGB8_SIZE); break; case RGBA: applyAlpha(image); object = new DefineImage2(identifier, width, height, zip(image)); break; default: throw new DataFormatException(BAD_FORMAT); } return object; } /** {@inheritDoc} */ public int getWidth() { return width; } /** {@inheritDoc} */ public int getHeight() { return height; } /** {@inheritDoc} */ public byte[] getImage() { return Arrays.copyOf(image, image.length); } /** * Decode a BufferedImage. * * @param obj * a BufferedImage. * * @throws IOException * if there is a problem extracting the image, from the * BufferedImage. * * @throws DataFormatException * if there is a problem decoding the BufferedImage. */ public void read(final BufferedImage obj) throws IOException, DataFormatException { final DataBuffer buffer = obj.getData().getDataBuffer(); width = obj.getWidth(); height = obj.getHeight(); if (buffer.getDataType() == DataBuffer.TYPE_INT) { decodeIntImage(obj); } else if (buffer.getDataType() == DataBuffer.TYPE_BYTE) { decodeByteImage(obj); } else if (buffer.getDataType() == DataBuffer.TYPE_USHORT) { decodeShortImage(obj); } else { throw new DataFormatException(BAD_FORMAT); } } /** * Decode a BufferedImage where each pixel is an integer value. * @param obj a BufferedImage. * @throws DataFormatException if the image cannot be decoded. */ private void decodeIntImage(final BufferedImage obj) throws DataFormatException { final DataBuffer buffer = obj.getData().getDataBuffer(); switch (obj.getType()) { case BufferedImage.TYPE_INT_ARGB: decodeARGB(buffer); break; case BufferedImage.TYPE_INT_ARGB_PRE: decodeARGBPre(buffer); break; case BufferedImage.TYPE_INT_BGR: decodeBGR(buffer); break; case BufferedImage.TYPE_INT_RGB: decodeRGB(buffer); break; default: throw new DataFormatException(BAD_FORMAT); } } /** * Decode the data from a BufferedImage where each pixel is in the format * ARGB. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeARGB(final DataBuffer buffer) { final int[] pixels = ((DataBufferInt) buffer).getData(); format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int pixel = pixels[y * width + x]; image[index + ALPHA] = (byte) (pixel >> ALIGN_BYTE4); image[index + BLUE] = (byte) (pixel >> ALIGN_BYTE3); image[index + GREEN] = (byte) (pixel >> ALIGN_BYTE2); image[index] = (byte) pixel; } } } /** * Decode the data from a BufferedImage where each pixel is in the format * ARGB with the alpha level already applied. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeARGBPre(final DataBuffer buffer) { final int[] pixels = ((DataBufferInt) buffer).getData(); format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int pixel = pixels[y * width + x]; image[index + ALPHA] = (byte) (pixel >> ALIGN_BYTE4); image[index + BLUE] = (byte) (pixel >> ALIGN_BYTE3); image[index + GREEN] = (byte) (pixel >> ALIGN_BYTE2); image[index] = (byte) pixel; } } } /** * Decode the data from a BufferedImage where each pixel is in the format * RGB. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeBGR(final DataBuffer buffer) { final int[] pixels = ((DataBufferInt) buffer).getData(); format = ImageFormat.RGB8; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int pixel = pixels[y * width + x]; image[index + ALPHA] = OPAQUE; image[index + BLUE] = (byte) (pixel >> ALIGN_BYTE3); image[index + GREEN] = (byte) (pixel >> ALIGN_BYTE2); image[index] = (byte) pixel; } } } /** * Decode the data from a BufferedImage where each pixel is in the format * RGB. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeRGB(final DataBuffer buffer) { final int[] pixels = ((DataBufferInt) buffer).getData(); format = ImageFormat.RGB8; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int pixel = pixels[y * width + x]; image[index + ALPHA] = OPAQUE; image[index] = (byte) (pixel >> ALIGN_BYTE3); image[index + GREEN] = (byte) (pixel >> ALIGN_BYTE2); image[index + BLUE] = (byte) pixel; } } } /** * Decode a BufferedImage where each pixel is an byte value. * @param obj a BufferedImage. * @throws IOException if there is an error decoding the pixel data. * @throws DataFormatException if the image cannot be decoded. */ private void decodeByteImage(final BufferedImage obj) throws IOException, DataFormatException { final DataBuffer buffer = obj.getData().getDataBuffer(); switch (obj.getType()) { case BufferedImage.TYPE_3BYTE_BGR: decodeByteBGR(buffer); break; case BufferedImage.TYPE_CUSTOM: decodeByteCustom(buffer); break; case BufferedImage.TYPE_4BYTE_ABGR: decodeByteABGR(buffer); break; case BufferedImage.TYPE_4BYTE_ABGR_PRE: decodeByteABGRPre(buffer); break; case BufferedImage.TYPE_BYTE_BINARY: decodeByteBinary(buffer, obj.getColorModel()); break; case BufferedImage.TYPE_BYTE_GRAY: decodeByteGray(buffer); break; case BufferedImage.TYPE_BYTE_INDEXED: decodeByteIndexed(buffer, obj.getColorModel()); break; default: throw new DataFormatException(BAD_FORMAT); } } /** * Decode the data from a BufferedImage where each pixel is in the format * BGR. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeByteBGR(final DataBuffer buffer) { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.RGB8; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int offset = 3 * (y * width + x); image[index + ALPHA] = OPAQUE; image[index + BLUE] = pixels[offset]; image[index + GREEN] = pixels[offset + 1]; image[index] = pixels[offset + 2]; } } } /** * Decode the data from a BufferedImage where each pixel is in a custom * format. * @param buffer the DataBuffer from a BufferedImage. * @throws DataFormatException if the image cannot be decoded. */ private void decodeByteCustom(final DataBuffer buffer) throws DataFormatException { final byte[] pixels = ((DataBufferByte) buffer).getData(); if (width * height * RGB_CHANNELS == pixels.length) { format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int offset = 3 * (y * width + x); image[index] = pixels[offset]; image[index + GREEN] = pixels[offset + 1]; image[index + BLUE] = pixels[offset + 2]; image[index + ALPHA] = OPAQUE; index += BYTES_PER_PIXEL; } } } else if (width * height * BYTES_PER_PIXEL == pixels.length) { format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int offset = BYTES_PER_PIXEL * (y * width + x); image[index] = pixels[offset]; image[index + GREEN] = pixels[offset + 1]; image[index + BLUE] = pixels[offset + 2]; image[index + ALPHA] = pixels[offset + ALPHA]; index += BYTES_PER_PIXEL; } } } else { throw new DataFormatException(BAD_FORMAT); } } /** * Decode the data from a BufferedImage where each pixel is in the format * ABGR. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeByteABGR(final DataBuffer buffer) { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int offset = BYTES_PER_PIXEL * (y * width + x); image[index + ALPHA] = pixels[offset]; image[index + BLUE] = pixels[offset + 1]; image[index + GREEN] = pixels[offset + 2]; image[index] = pixels[offset + ALPHA]; } } } /** * Decode the data from a BufferedImage where each pixel is in the format * ABGR with the alpha level pre-applied. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeByteABGRPre(final DataBuffer buffer) { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.RGBA; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++, index += BYTES_PER_PIXEL) { final int offset = BYTES_PER_PIXEL * (y * width + x); image[index + ALPHA] = pixels[offset]; image[index + BLUE] = pixels[offset + 1]; image[index + GREEN] = pixels[offset + 2]; image[index] = pixels[offset + ALPHA]; } } } /** * Decode the data from a BufferedImage where each pixel is either black * or white. * @param buffer the DataBuffer from a BufferedImage. * @param model the ColorModel from a BufferedImage. * @throws IOException if there is an error decoding the pixel data. */ private void decodeByteBinary(final DataBuffer buffer, final ColorModel model) throws IOException { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.IDX8; image = new byte[height * width]; final int depth = model.getPixelSize(); decodeColorTable(model); int index = 0; final ByteArrayInputStream stream = new ByteArrayInputStream(pixels); final LittleDecoder coder = new LittleDecoder(stream); for (int y = 0; y < height; y++) { // coder.mark(); for (int x = 0; x < width; x++, index++) { image[index] = (byte) coder.readBits(depth, false); } // coder.alignToWord(); // coder.unmark(); } } /** * Decode the data from a BufferedImage where each pixel is a greyscale * value. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeByteGray(final DataBuffer buffer) { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.RGB8; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int offset = (y * width + x); image[index + ALPHA] = OPAQUE; image[index + BLUE] = pixels[offset]; image[index + GREEN] = pixels[offset]; image[index] = pixels[offset]; index += BYTES_PER_PIXEL; } } } /** * Decode the data from a BufferedImage where each pixel an index into a * colour table. * @param buffer the DataBuffer from a BufferedImage. * @param model the ColorModel from a BufferedImage. */ private void decodeByteIndexed(final DataBuffer buffer, final ColorModel model) { final byte[] pixels = ((DataBufferByte) buffer).getData(); format = ImageFormat.IDX8; image = new byte[height * width]; decodeColorTable(model); int index = 0; for (int y = 0; y < height; y++) { System.arraycopy(pixels, y * width, image, index, width); index += width; } } /** * Decode a BufferedImage where each pixel is a 16-bit value. * @param obj a BufferedImage. * @throws DataFormatException if the image cannot be decoded. */ private void decodeShortImage(final BufferedImage obj) throws DataFormatException { final DataBuffer buffer = obj.getData().getDataBuffer(); switch (obj.getType()) { case BufferedImage.TYPE_USHORT_555_RGB: throw new DataFormatException(BAD_FORMAT); case BufferedImage.TYPE_USHORT_565_RGB: throw new DataFormatException(BAD_FORMAT); case BufferedImage.TYPE_USHORT_GRAY: decodeShortGray(buffer); break; // case BufferedImage.TYPE_CUSTOM: // decodeShortCustom(buffer); // break; default: throw new DataFormatException(BAD_FORMAT); } } /** * Decode the data from a BufferedImage where each pixel is a greyscale * value. * @param buffer the DataBuffer from a BufferedImage. */ private void decodeShortGray(final DataBuffer buffer) { final short[] pixels = ((DataBufferUShort) buffer).getData(); //NOPMD format = ImageFormat.RGB8; image = new byte[height * width * BYTES_PER_PIXEL]; int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // int row = height-y-1; final int offset = (y * width + x); image[index + ALPHA] = OPAQUE; image[index + BLUE] = (byte) pixels[offset]; image[index + GREEN] = (byte) pixels[offset]; image[index] = (byte) pixels[offset]; index += BYTES_PER_PIXEL; } } } // private void decodeShortCustom(final DataBuffer buffer) { // final short[] pixels = ((DataBufferUShort) buffer).getData(); // // if (width * height * RGB_CHANNELS == pixels.length) { // format = ImageFormat.RGBA; // image = new byte[height * width * BYTES_PER_PIXEL]; // int index = 0; // // for (int y = 0; y < height; y++) { // for (int x = 0; x < width; x++) { // final int offset = 3 * (y * width + x); // // image[index] = (byte) pixels[offset]; // image[index + GREEN] = (byte) pixels[offset + 1]; // image[index + BLUE] = (byte) pixels[offset + 2]; // image[index + ALPHA] = OPAQUE; // index += BYTES_PER_PIXEL; // } // } // } else if (width * height * BYTES_PER_PIXEL == pixels.length) { // format = ImageFormat.RGBA; // image = new byte[height * width * BYTES_PER_PIXEL]; // int index = 0; // for (int y = 0; y < height; y++) { // for (int x = 0; x < width; x++) { // final int offset = BYTES_PER_PIXEL // * (y * width + x); // // image[index] = (byte) pixels[offset]; // image[index + GREEN] = (byte) pixels[offset + 1]; // image[index + BLUE] = (byte) pixels[offset + 2]; // image[index + ALPHA] = (byte) pixels[offset + ALPHA]; // index += BYTES_PER_PIXEL; // } // } // } // } /** * Decode the ColourModel used for indexed images. * @param model The ColorModel used by a BufferedImage. */ private void decodeColorTable(final ColorModel model) { if (model instanceof IndexColorModel) { final IndexColorModel indexModel = (IndexColorModel) model; table = new byte[indexModel.getMapSize() * COLOUR_CHANNELS]; final byte[] reds = new byte[indexModel.getMapSize()]; final byte[] blues = new byte[indexModel.getMapSize()]; final byte[] greens = new byte[indexModel.getMapSize()]; indexModel.getReds(reds); indexModel.getGreens(greens); indexModel.getBlues(blues); int index = 0; for (int i = 0; i < table.length; i += COLOUR_CHANNELS) { table[i + RED] = reds[index]; table[i + GREEN] = greens[index]; table[i + BLUE] = blues[index]; table[i + ALPHA] = -1; index++; } } } /** * Reorder the image pixels from RGBA to ARGB. * * @param img the image data. */ private void orderAlpha(final byte[] img) { byte alpha; for (int i = 0; i < img.length; i += BYTES_PER_PIXEL) { alpha = img[i + ALPHA]; img[i + ALPHA] = img[i + BLUE]; img[i + BLUE] = img[i + GREEN]; img[i + GREEN] = img[i]; img[i] = alpha; } } /** * Apply the level for the alpha channel to the red, green and blue colour * channels for encoding the image so it can be added to a Flash movie. * @param img the image data. */ private void applyAlpha(final byte[] img) { int alpha; for (int i = 0; i < img.length; i += BYTES_PER_PIXEL) { alpha = img[i + ALPHA] & MASK_8BIT; img[i + ALPHA] = (byte) (((img[i + BLUE] & MASK_8BIT) * alpha) / OPAQUE); img[i + BLUE] = (byte) (((img[i + GREEN] & MASK_8BIT) * alpha) / OPAQUE); img[i + GREEN] = (byte) (((img[i + RED] & MASK_8BIT) * alpha) / OPAQUE); img[i + RED] = (byte) alpha; } } /** * Concatenate the colour table and the image data together. * @param img the image data. * @param colors the colour table. * @return a single array containing the red, green and blue (not alpha) * entries from the colour table followed by the red, green, blue and * alpha channels from the image. The alpha defaults to 255 for an opaque * image. */ private byte[] merge(final byte[] img, final byte[] colors) { final byte[] merged = new byte[(colors.length / BYTES_PER_PIXEL) * RGB_CHANNELS + img.length]; int dst = 0; for (int i = 0; i < colors.length; i += BYTES_PER_PIXEL) { merged[dst++] = colors[i + RED]; merged[dst++] = colors[i + GREEN]; merged[dst++] = colors[i + BLUE]; } for (final byte element : img) { merged[dst++] = element; } return merged; } /** * Concatenate the colour table and the image data together. * @param img the image data. * @param colors the colour table. * @return a single array containing entries from the colour table followed * by the image. */ private byte[] mergeAlpha(final byte[] img, final byte[] colors) { final byte[] merged = new byte[colors.length + img.length]; int dst = 0; for (final byte element : colors) { merged[dst++] = element; } for (final byte element : img) { merged[dst++] = element; } return merged; } /** * Compress the image using the ZIP format. * @param img the image data. * @return the compressed image. */ private byte[] zip(final byte[] img) { final Deflater deflater = new Deflater(); deflater.setInput(img); deflater.finish(); final byte[] compressedData = new byte[img.length * 2]; final int bytesCompressed = deflater.deflate(compressedData); final byte[] newData = Arrays.copyOf(compressedData, bytesCompressed); return newData; } /** * Adjust the width of each row in an image so the data is aligned to a * 16-bit word boundary when loaded in memory. The additional bytes are * all set to zero and will not be displayed in the image. * * @param imgWidth the width of the image in pixels. * @param imgHeight the height of the image in pixels. * @param img the image data. * @return the image data with each row aligned to a 16-bit boundary. */ private byte[] adjustScan(final int imgWidth, final int imgHeight, final byte[] img) { int src = 0; int dst = 0; int row; int col; int scan = 0; byte[] formattedImage = null; scan = (imgWidth + WORD_ALIGN) & ~WORD_ALIGN; formattedImage = new byte[scan * imgHeight]; for (row = 0; row < imgHeight; row++) { for (col = 0; col < imgWidth; col++) { formattedImage[dst++] = img[src++]; } while (col++ < scan) { formattedImage[dst++] = 0; } } return formattedImage; } /** * Convert an image with 32-bits for the red, green, blue and alpha channels * to one where the channels each take 5-bits in a 16-bit word. * @param imgWidth the width of the image in pixels. * @param imgHeight the height of the image in pixels. * @param img the image data. * @return the image data with the red, green and blue channels packed into * 16-bit words. Alpha is discarded. */ private byte[] packColours(final int imgWidth, final int imgHeight, final byte[] img) { int src = 0; int dst = 0; int row; int col; final int scan = imgWidth + (imgWidth & 1); final byte[] formattedImage = new byte[scan * imgHeight * 2]; for (row = 0; row < imgHeight; row++) { for (col = 0; col < imgWidth; col++, src++) { final int red = (img[src++] & RGB5_MSB_MASK) << RGB5_RED_SHIFT; final int green = (img[src++] & RGB5_MSB_MASK) << RGB5_GREEN_SHIFT; final int blue = (img[src++] & RGB5_MSB_MASK) >> RGB5_BLUE_SHIFT; final int colour = (red | green | blue) & Coder.LOWEST15; formattedImage[dst++] = (byte) (colour >> ALIGN_BYTE2); formattedImage[dst++] = (byte) colour; } while (col < scan) { formattedImage[dst++] = 0; formattedImage[dst++] = 0; col++; } } return formattedImage; } }