/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.imaging.formats.gif; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.imaging.FormatCompliance; import org.apache.commons.imaging.ImageFormat; import org.apache.commons.imaging.ImageInfo; import org.apache.commons.imaging.ImageParser; import org.apache.commons.imaging.ImageReadException; import org.apache.commons.imaging.ImageWriteException; import org.apache.commons.imaging.common.BinaryOutputStream; import org.apache.commons.imaging.common.ByteOrder; import org.apache.commons.imaging.common.IImageMetadata; import org.apache.commons.imaging.common.ImageBuilder; import org.apache.commons.imaging.common.bytesource.ByteSource; import org.apache.commons.imaging.common.mylzw.MyLzwCompressor; import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor; import org.apache.commons.imaging.palette.Palette; import org.apache.commons.imaging.palette.PaletteFactory; import org.apache.commons.imaging.util.Debug; import org.apache.commons.imaging.util.ParamMap; public class GifImageParser extends ImageParser { public GifImageParser() { super.setByteOrder(ByteOrder.LITTLE_ENDIAN); } @Override public String getName() { return "Gif-Custom"; } @Override public String getDefaultExtension() { return DEFAULT_EXTENSION; } private static final String DEFAULT_EXTENSION = ".gif"; private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, }; @Override protected String[] getAcceptedExtensions() { return ACCEPTED_EXTENSIONS; } @Override protected ImageFormat[] getAcceptedTypes() { return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_GIF, // }; } private static final byte GIF_HEADER_SIGNATURE[] = { 71, 73, 70 }; private GifHeaderInfo readHeader(final InputStream is, final FormatCompliance formatCompliance) throws ImageReadException, IOException { final byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File"); final byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File"); final byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File"); final byte version1 = readByte("version1", is, "Not a Valid GIF File"); final byte version2 = readByte("version2", is, "Not a Valid GIF File"); final byte version3 = readByte("version3", is, "Not a Valid GIF File"); if (formatCompliance != null) { formatCompliance.compare_bytes("Signature", GIF_HEADER_SIGNATURE, new byte[] { identifier1, identifier2, identifier3, }); formatCompliance.compare("version", 56, version1); formatCompliance .compare("version", new int[] { 55, 57, }, version2); formatCompliance.compare("version", 97, version3); } if (debug) { printCharQuad("identifier: ", ((identifier1 << 16) | (identifier2 << 8) | (identifier3 << 0))); printCharQuad("version: ", ((version1 << 16) | (version2 << 8) | (version3 << 0))); } final int logicalScreenWidth = read2Bytes("Logical Screen Width", is, "Not a Valid GIF File"); final int logicalScreenHeight = read2Bytes("Logical Screen Height", is, "Not a Valid GIF File"); if (formatCompliance != null) { formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, logicalScreenWidth); formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, logicalScreenHeight); } final byte packedFields = readByte("Packed Fields", is, "Not a Valid GIF File"); final byte backgroundColorIndex = readByte("Background Color Index", is, "Not a Valid GIF File"); final byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, "Not a Valid GIF File"); if (debug) { printByteBits("PackedFields bits", packedFields); } final boolean globalColorTableFlag = ((packedFields & 128) > 0); if (debug) { System.out.println("GlobalColorTableFlag: " + globalColorTableFlag); } final byte colorResolution = (byte) ((packedFields >> 4) & 7); if (debug) { System.out.println("ColorResolution: " + colorResolution); } final boolean sortFlag = ((packedFields & 8) > 0); if (debug) { System.out.println("SortFlag: " + sortFlag); } final byte sizeofGlobalColorTable = (byte) (packedFields & 7); if (debug) { System.out.println("SizeofGlobalColorTable: " + sizeofGlobalColorTable); } if (formatCompliance != null) { if (globalColorTableFlag && backgroundColorIndex != -1) { formatCompliance.checkBounds("Background Color Index", 0, convertColorTableSize(sizeofGlobalColorTable), backgroundColorIndex); } } return new GifHeaderInfo(identifier1, identifier2, identifier3, version1, version2, version3, logicalScreenWidth, logicalScreenHeight, packedFields, backgroundColorIndex, pixelAspectRatio, globalColorTableFlag, colorResolution, sortFlag, sizeofGlobalColorTable); } private GraphicControlExtension readGraphicControlExtension(final int code, final InputStream is) throws ImageReadException, IOException { readByte("block_size", is, "GIF: corrupt GraphicControlExt"); final int packed = readByte("packed fields", is, "GIF: corrupt GraphicControlExt"); final int dispose = (packed & 0x1c) >> 2; // disposal method final boolean transparency = (packed & 1) != 0; final int delay = read2Bytes("delay in milliseconds", is, "GIF: corrupt GraphicControlExt"); final int transparentColorIndex = 0xff & readByte("transparent color index", is, "GIF: corrupt GraphicControlExt"); readByte("block terminator", is, "GIF: corrupt GraphicControlExt"); return new GraphicControlExtension(code, packed, dispose, transparency, delay, transparentColorIndex); } private byte[] readSubBlock(final InputStream is) throws IOException { final int block_size = 0xff & readByte("block_size", is, "GIF: corrupt block"); final byte bytes[] = readBytes("block", is, block_size, "GIF: corrupt block"); return bytes; } protected GenericGifBlock readGenericGIFBlock(final InputStream is, final int code) throws IOException { return readGenericGIFBlock(is, code, null); } protected GenericGifBlock readGenericGIFBlock(final InputStream is, final int code, final byte first[]) throws IOException { final List<byte[]> subblocks = new ArrayList<byte[]>(); if (first != null) { subblocks.add(first); } while (true) { final byte bytes[] = readSubBlock(is); if (bytes.length < 1) { break; } subblocks.add(bytes); } return new GenericGifBlock(code, subblocks); } private final static int EXTENSION_CODE = 0x21; private final static int IMAGE_SEPARATOR = 0x2C; private final static int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9; private final static int COMMENT_EXTENSION = 0xfe; private final static int PLAIN_TEXT_EXTENSION = 0x01; private final static int XMP_EXTENSION = 0xff; private final static int TERMINATOR_BYTE = 0x3b; private final static int APPLICATION_EXTENSION_LABEL = 0xff; private final static int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8) | XMP_EXTENSION; private List<GifBlock> readBlocks(final GifHeaderInfo ghi, final InputStream is, final boolean stopBeforeImageData, final FormatCompliance formatCompliance) throws ImageReadException, IOException { final List<GifBlock> result = new ArrayList<GifBlock>(); while (true) { final int code = is.read(); switch (code) { case -1: throw new ImageReadException("GIF: unexpected end of data"); case IMAGE_SEPARATOR: final ImageDescriptor id = readImageDescriptor(ghi, code, is, stopBeforeImageData, formatCompliance); result.add(id); // if(stopBeforeImageData) // return result; break; case EXTENSION_CODE: // extension { final int extensionCode = is.read(); final int completeCode = ((0xff & code) << 8) | (0xff & extensionCode); switch (extensionCode) { case 0xf9: final GraphicControlExtension gce = readGraphicControlExtension( completeCode, is); result.add(gce); break; case COMMENT_EXTENSION: case PLAIN_TEXT_EXTENSION: { final GenericGifBlock block = readGenericGIFBlock(is, completeCode); result.add(block); break; } case APPLICATION_EXTENSION_LABEL: // 255 (hex 0xFF) Application // Extension Label { final byte label[] = readSubBlock(is); if (formatCompliance != null) { formatCompliance.addComment( "Unknown Application Extension (" + new String(label, "US-ASCII") + ")", completeCode); } // if (label == new String("ICCRGBG1")) { // GIF's can have embedded ICC Profiles - who knew? } if ((label != null) && (label.length > 0)) { final GenericGifBlock block = readGenericGIFBlock(is, completeCode, label); result.add(block); } break; } default: { if (formatCompliance != null) { formatCompliance.addComment("Unknown block", completeCode); } final GenericGifBlock block = readGenericGIFBlock(is, completeCode); result.add(block); break; } } } break; case TERMINATOR_BYTE: return result; case 0x00: // bad byte, but keep going and see what happens break; default: throw new ImageReadException("GIF: unknown code: " + code); } } } private ImageDescriptor readImageDescriptor(final GifHeaderInfo ghi, final int blockCode, final InputStream is, final boolean stopBeforeImageData, final FormatCompliance formatCompliance) throws ImageReadException, IOException { final int ImageLeftPosition = read2Bytes("Image Left Position", is, "Not a Valid GIF File"); final int ImageTopPosition = read2Bytes("Image Top Position", is, "Not a Valid GIF File"); final int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File"); final int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File"); final byte PackedFields = readByte("Packed Fields", is, "Not a Valid GIF File"); if (formatCompliance != null) { formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth, imageWidth); formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight, imageHeight); formatCompliance.checkBounds("Left Position", 0, ghi.logicalScreenWidth - imageWidth, ImageLeftPosition); formatCompliance.checkBounds("Top Position", 0, ghi.logicalScreenHeight - imageHeight, ImageTopPosition); } if (debug) { printByteBits("PackedFields bits", PackedFields); } final boolean LocalColorTableFlag = (((PackedFields >> 7) & 1) > 0); if (debug) { System.out.println("LocalColorTableFlag: " + LocalColorTableFlag); } final boolean InterlaceFlag = (((PackedFields >> 6) & 1) > 0); if (debug) { System.out.println("Interlace Flag: " + InterlaceFlag); } final boolean SortFlag = (((PackedFields >> 5) & 1) > 0); if (debug) { System.out.println("Sort Flag: " + SortFlag); } final byte SizeofLocalColorTable = (byte) (PackedFields & 7); if (debug) { System.out.println("SizeofLocalColorTable: " + SizeofLocalColorTable); } byte LocalColorTable[] = null; if (LocalColorTableFlag) { LocalColorTable = readColorTable(is, SizeofLocalColorTable, formatCompliance); } byte imageData[] = null; if (!stopBeforeImageData) { final int LZWMinimumCodeSize = is.read(); final GenericGifBlock block = readGenericGIFBlock(is, -1); final byte bytes[] = block.appendSubBlocks(); final InputStream bais = new ByteArrayInputStream(bytes); final int size = imageWidth * imageHeight; final MyLzwDecompressor myLzwDecompressor = new MyLzwDecompressor( LZWMinimumCodeSize, ByteOrder.LITTLE_ENDIAN); imageData = myLzwDecompressor.decompress(bais, size); } else { final int LZWMinimumCodeSize = is.read(); if (debug) { System.out.println("LZWMinimumCodeSize: " + LZWMinimumCodeSize); } readGenericGIFBlock(is, -1); } final ImageDescriptor result = new ImageDescriptor(blockCode, ImageLeftPosition, ImageTopPosition, imageWidth, imageHeight, PackedFields, LocalColorTableFlag, InterlaceFlag, SortFlag, SizeofLocalColorTable, LocalColorTable, imageData); return result; } private int simple_pow(final int base, final int power) { int result = 1; for (int i = 0; i < power; i++) { result *= base; } return result; } private int convertColorTableSize(final int ct_size) { return 3 * simple_pow(2, ct_size + 1); } private byte[] readColorTable(final InputStream is, final int ct_size, final FormatCompliance formatCompliance) throws IOException { final int actual_size = convertColorTableSize(ct_size); final byte bytes[] = readBytes("block", is, actual_size, "GIF: corrupt Color Table"); return bytes; } private GifBlock findBlock(final List<GifBlock> v, final int code) { for (int i = 0; i < v.size(); i++) { final GifBlock gifBlock = v.get(i); if (gifBlock.blockCode == code) { return gifBlock; } } return null; } private ImageContents readFile(final ByteSource byteSource, final boolean stopBeforeImageData) throws ImageReadException, IOException { return readFile(byteSource, stopBeforeImageData, FormatCompliance.getDefault()); } private ImageContents readFile(final ByteSource byteSource, final boolean stopBeforeImageData, final FormatCompliance formatCompliance) throws ImageReadException, IOException { InputStream is = null; try { is = byteSource.getInputStream(); final GifHeaderInfo ghi = readHeader(is, formatCompliance); byte globalColorTable[] = null; if (ghi.globalColorTableFlag) { globalColorTable = readColorTable(is, ghi.sizeOfGlobalColorTable, formatCompliance); } final List<GifBlock> blocks = readBlocks(ghi, is, stopBeforeImageData, formatCompliance); final ImageContents result = new ImageContents(ghi, globalColorTable, blocks); return result; } finally { try { if (is != null) { is.close(); } } catch (final Exception e) { Debug.debug(e); } } } @Override public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { return null; } @Override public Dimension getImageSize(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { final ImageContents blocks = readFile(byteSource, false); if (blocks == null) { throw new ImageReadException("GIF: Couldn't read blocks"); } final GifHeaderInfo bhi = blocks.gifHeaderInfo; if (bhi == null) { throw new ImageReadException("GIF: Couldn't read Header"); } final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, IMAGE_SEPARATOR); if (id == null) { throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); } // Prefer the size information in the ImageDescriptor; it is more // reliable // than the size information in the header. return new Dimension(id.imageWidth, id.imageHeight); } public byte[] embedICCProfile(final byte image[], final byte profile[]) { return null; } @Override public boolean embedICCProfile(final File src, final File dst, final byte profile[]) { return false; } @Override public IImageMetadata getMetadata(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { return null; } private List<String> getComments(final List<GifBlock> v) throws IOException { final List<String> result = new ArrayList<String>(); final int code = 0x21fe; for (int i = 0; i < v.size(); i++) { final GifBlock block = v.get(i); if (block.blockCode == code) { final byte bytes[] = ((GenericGifBlock) block).appendSubBlocks(); result.add(new String(bytes, "US-ASCII")); } } return result; } @Override public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { final ImageContents blocks = readFile(byteSource, false); if (blocks == null) { throw new ImageReadException("GIF: Couldn't read blocks"); } final GifHeaderInfo bhi = blocks.gifHeaderInfo; if (bhi == null) { throw new ImageReadException("GIF: Couldn't read Header"); } final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, IMAGE_SEPARATOR); if (id == null) { throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); } final GraphicControlExtension gce = (GraphicControlExtension) findBlock( blocks.blocks, GRAPHIC_CONTROL_EXTENSION); // Prefer the size information in the ImageDescriptor; it is more // reliable than the size information in the header. final int height = id.imageHeight; final int width = id.imageWidth; final List<String> comments = getComments(blocks.blocks); final int bitsPerPixel = (bhi.colorResolution + 1); final ImageFormat format = ImageFormat.IMAGE_FORMAT_GIF; final String formatName = "GIF Graphics Interchange Format"; final String mimeType = "image/gif"; // we ought to count images, but don't yet. final int numberOfImages = -1; final boolean isProgressive = id.interlaceFlag; final int physicalWidthDpi = 72; final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); final int physicalHeightDpi = 72; final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); final String formatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1) + ((char) blocks.gifHeaderInfo.version2) + ((char) blocks.gifHeaderInfo.version3); boolean isTransparent = false; if (gce != null && gce.transparency) { isTransparent = true; } final boolean usesPalette = true; final int colorType = ImageInfo.COLOR_TYPE_RGB; final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; final ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch, physicalWidthDpi, physicalWidthInch, width, isProgressive, isTransparent, usesPalette, colorType, compressionAlgorithm); return result; } @Override public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImageReadException, IOException { pw.println("gif.dumpImageFile"); final ImageInfo imageData = getImageInfo(byteSource); if (imageData == null) { return false; } imageData.toString(pw, ""); final ImageContents blocks = readFile(byteSource, false); pw.println("gif.blocks: " + blocks.blocks.size()); for (int i = 0; i < blocks.blocks.size(); i++) { final GifBlock gifBlock = blocks.blocks.get(i); this.debugNumber(pw, "\t" + i + " (" + gifBlock.getClass().getName() + ")", gifBlock.blockCode, 4); } pw.println(""); return true; } private int[] getColorTable(final byte bytes[]) throws ImageReadException { if ((bytes.length % 3) != 0) { throw new ImageReadException("Bad Color Table Length: " + bytes.length); } final int length = bytes.length / 3; final int result[] = new int[length]; for (int i = 0; i < length; i++) { final int red = 0xff & bytes[(i * 3) + 0]; final int green = 0xff & bytes[(i * 3) + 1]; final int blue = 0xff & bytes[(i * 3) + 2]; final int alpha = 0xff; final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); result[i] = rgb; } return result; } @Override public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImageReadException, IOException { final FormatCompliance result = new FormatCompliance( byteSource.getDescription()); readFile(byteSource, false, result); return result; } @Override public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { final ImageContents imageContents = readFile(byteSource, false); if (imageContents == null) { throw new ImageReadException("GIF: Couldn't read blocks"); } final GifHeaderInfo ghi = imageContents.gifHeaderInfo; if (ghi == null) { throw new ImageReadException("GIF: Couldn't read Header"); } final ImageDescriptor id = (ImageDescriptor) findBlock(imageContents.blocks, IMAGE_SEPARATOR); if (id == null) { throw new ImageReadException("GIF: Couldn't read Image Descriptor"); } final GraphicControlExtension gce = (GraphicControlExtension) findBlock( imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); // Prefer the size information in the ImageDescriptor; it is more // reliable // than the size information in the header. final int width = id.imageWidth; final int height = id.imageHeight; boolean hasAlpha = false; if (gce != null && gce.transparency) { hasAlpha = true; } final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha); int colorTable[]; if (id.localColorTable != null) { colorTable = getColorTable(id.localColorTable); } else if (imageContents.globalColorTable != null) { colorTable = getColorTable(imageContents.globalColorTable); } else { throw new ImageReadException("Gif: No Color Table"); } int transparentIndex = -1; if (hasAlpha) { transparentIndex = gce.transparentColorIndex; } int counter = 0; final int rowsInPass1 = (height + 7) / 8; final int rowsInPass2 = (height + 3) / 8; final int rowsInPass3 = (height + 1) / 4; final int rowsInPass4 = (height) / 2; for (int row = 0; row < height; row++) { int y; if (id.interlaceFlag) { int the_row = row; if (the_row < rowsInPass1) { y = the_row * 8; } else { the_row -= rowsInPass1; if (the_row < (rowsInPass2)) { y = 4 + (the_row * 8); } else { the_row -= rowsInPass2; if (the_row < (rowsInPass3)) { y = 2 + (the_row * 4); } else { the_row -= rowsInPass3; if (the_row < (rowsInPass4)) { y = 1 + (the_row * 2); } else { throw new ImageReadException( "Gif: Strange Row"); } } } } } else { y = row; } for (int x = 0; x < width; x++) { final int index = 0xff & id.imageData[counter++]; int rgb = colorTable[index]; if (transparentIndex == index) { rgb = 0x00; } imageBuilder.setRGB(x, y, rgb); } } return imageBuilder.getBufferedImage(); } private void writeAsSubBlocks(final OutputStream os, final byte bytes[]) throws IOException { int index = 0; while (index < bytes.length) { final int block_size = Math.min(bytes.length - index, 255); os.write(block_size); os.write(bytes, index, block_size); index += block_size; } os.write(0); // last block } private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7; private static final int INTERLACE_FLAG_MASK = 1 << 6; private static final int SORT_FLAG_MASK = 1 << 5; @Override public void writeImage(final BufferedImage src, final OutputStream os, Map<String,Object> params) throws ImageWriteException, IOException { // make copy of params; we'll clear keys as we consume them. params = new HashMap<String,Object>(params); final boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, false); // clear format key. if (params.containsKey(PARAM_KEY_FORMAT)) { params.remove(PARAM_KEY_FORMAT); } if (params.containsKey(PARAM_KEY_VERBOSE)) { params.remove(PARAM_KEY_VERBOSE); } String xmpXml = null; if (params.containsKey(PARAM_KEY_XMP_XML)) { xmpXml = (String) params.get(PARAM_KEY_XMP_XML); params.remove(PARAM_KEY_XMP_XML); } if (params.size() > 0) { final Object firstKey = params.keySet().iterator().next(); throw new ImageWriteException("Unknown parameter: " + firstKey); } final int width = src.getWidth(); final int height = src.getHeight(); final boolean hasAlpha = new PaletteFactory().hasTransparency(src); final int max_colors = hasAlpha ? 255 : 256; Palette palette2 = new PaletteFactory().makeExactRgbPaletteSimple(src, max_colors); // int palette[] = new PaletteFactory().makePaletteSimple(src, 256); // Map palette_map = paletteToMap(palette); if (palette2 == null) { palette2 = new PaletteFactory().makeQuantizedRgbPalette(src, max_colors); if (verbose) { System.out.println("quantizing"); } } else if (verbose) { System.out.println("exact palette"); } if (palette2 == null) { throw new ImageWriteException( "Gif: can't write images with more than 256 colors"); } final int palette_size = palette2.length() + (hasAlpha ? 1 : 0); final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); // write Header os.write(0x47); // G magic numbers os.write(0x49); // I os.write(0x46); // F os.write(0x38); // 8 version magic numbers os.write(0x39); // 9 os.write(0x61); // a // Logical Screen Descriptor. bos.write2Bytes(width); bos.write2Bytes(height); final int colorTableScaleLessOne = (palette_size > 128) ? 7 : (palette_size > 64) ? 6 : (palette_size > 32) ? 5 : (palette_size > 16) ? 4 : (palette_size > 8) ? 3 : (palette_size > 4) ? 2 : (palette_size > 2) ? 1 : 0; final int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1); { final byte colorResolution = (byte) colorTableScaleLessOne; // TODO: final boolean globalColorTableFlag = false; final boolean sortFlag = false; final int globalColorTableFlagMask = 1 << 7; final int sortFlagMask = 8; final int sizeOfGlobalColorTable = 0; final int packedFields = ((globalColorTableFlag ? globalColorTableFlagMask : 0) | (sortFlag ? sortFlagMask : 0) | ((7 & colorResolution) << 4) | (7 & sizeOfGlobalColorTable)); bos.write(packedFields); // one byte } { final byte BackgroundColorIndex = 0; bos.write(BackgroundColorIndex); } { final byte PixelAspectRatio = 0; bos.write(PixelAspectRatio); } { // write Global Color Table. } { // ALWAYS write GraphicControlExtension bos.write(EXTENSION_CODE); bos.write((byte) 0xf9); // bos.write(0xff & (kGraphicControlExtension >> 8)); // bos.write(0xff & (kGraphicControlExtension >> 0)); bos.write((byte) 4); // block size; final int packedFields = hasAlpha ? 1 : 0; // transparency flag bos.write((byte) packedFields); bos.write((byte) 0); // Delay Time bos.write((byte) 0); // Delay Time bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent // Color // Index bos.write((byte) 0); // terminator } if (null != xmpXml) { bos.write(EXTENSION_CODE); bos.write(APPLICATION_EXTENSION_LABEL); bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE); final byte xmpXmlBytes[] = xmpXml.getBytes("utf-8"); bos.write(xmpXmlBytes); // write "magic trailer" for (int magic = 0; magic <= 0xff; magic++) { bos.write(0xff - magic); } bos.write((byte) 0); // terminator } { // Image Descriptor. bos.write(IMAGE_SEPARATOR); bos.write2Bytes(0); // Image Left Position bos.write2Bytes(0); // Image Top Position bos.write2Bytes(width); // Image Width bos.write2Bytes(height); // Image Height { final boolean LocalColorTableFlag = true; // boolean LocalColorTableFlag = false; final boolean InterlaceFlag = false; final boolean SortFlag = false; final int SizeOfLocalColorTable = colorTableScaleLessOne; // int SizeOfLocalColorTable = 0; final int PackedFields = ((LocalColorTableFlag ? LOCAL_COLOR_TABLE_FLAG_MASK : 0) | (InterlaceFlag ? INTERLACE_FLAG_MASK : 0) | (SortFlag ? SORT_FLAG_MASK : 0) | (7 & SizeOfLocalColorTable)); bos.write(PackedFields); // one byte } } { // write Local Color Table. for (int i = 0; i < colorTableSizeInFormat; i++) { if (i < palette2.length()) { final int rgb = palette2.getEntry(i); final int red = 0xff & (rgb >> 16); final int green = 0xff & (rgb >> 8); final int blue = 0xff & (rgb >> 0); bos.write(red); bos.write(green); bos.write(blue); } else { bos.write(0); bos.write(0); bos.write(0); } } } { // get Image Data. // int image_data_total = 0; int LZWMinimumCodeSize = colorTableScaleLessOne + 1; // LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize); if (LZWMinimumCodeSize < 2) { LZWMinimumCodeSize = 2; } // TODO: // make // better // choice // here. bos.write(LZWMinimumCodeSize); final MyLzwCompressor compressor = new MyLzwCompressor( LZWMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false); // GIF // Mode); final byte imagedata[] = new byte[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int argb = src.getRGB(x, y); final int rgb = 0xffffff & argb; int index; if (hasAlpha) { final int alpha = 0xff & (argb >> 24); final int alphaThreshold = 255; if (alpha < alphaThreshold) { index = palette2.length(); // is transparent } else { index = palette2.getPaletteIndex(rgb); } } else { index = palette2.getPaletteIndex(rgb); } imagedata[y * width + x] = (byte) index; } } final byte compressed[] = compressor.compress(imagedata); writeAsSubBlocks(bos, compressed); // image_data_total += compressed.length; } // palette2.dump(); bos.write(TERMINATOR_BYTE); bos.close(); os.close(); } private static final byte XMP_APPLICATION_ID_AND_AUTH_CODE[] = { 0x58, // X 0x4D, // M 0x50, // P 0x20, // 0x44, // D 0x61, // a 0x74, // t 0x61, // a 0x58, // X 0x4D, // M 0x50, // P }; /** * Extracts embedded XML metadata as XML string. * <p> * * @param byteSource * File containing image data. * @param params * Map of optional parameters, defined in SanselanConstants. * @return Xmp Xml as String, if present. Otherwise, returns null. */ @Override public String getXmpXml(final ByteSource byteSource, final Map<String,Object> params) throws ImageReadException, IOException { InputStream is = null; try { is = byteSource.getInputStream(); final FormatCompliance formatCompliance = null; final GifHeaderInfo ghi = readHeader(is, formatCompliance); if (ghi.globalColorTableFlag) { readColorTable(is, ghi.sizeOfGlobalColorTable, formatCompliance); } final List<GifBlock> blocks = readBlocks(ghi, is, true, formatCompliance); final List<String> result = new ArrayList<String>(); for (int i = 0; i < blocks.size(); i++) { final GifBlock block = blocks.get(i); if (block.blockCode != XMP_COMPLETE_CODE) { continue; } final GenericGifBlock genericBlock = (GenericGifBlock) block; final byte blockBytes[] = genericBlock.appendSubBlocks(true); if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) { continue; } if (!compareBytes(blockBytes, 0, XMP_APPLICATION_ID_AND_AUTH_CODE, 0, XMP_APPLICATION_ID_AND_AUTH_CODE.length)) { continue; } final byte GIF_MAGIC_TRAILER[] = new byte[256]; for (int magic = 0; magic <= 0xff; magic++) { GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic); } if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length) { continue; } if (!compareBytes(blockBytes, blockBytes.length - GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0, GIF_MAGIC_TRAILER.length)) { throw new ImageReadException( "XMP block in GIF missing magic trailer."); } try { // XMP is UTF-8 encoded xml. final String xml = new String( blockBytes, XMP_APPLICATION_ID_AND_AUTH_CODE.length, blockBytes.length - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length), "utf-8"); result.add(xml); } catch (final UnsupportedEncodingException e) { throw new ImageReadException("Invalid XMP Block in GIF."); } } if (result.size() < 1) { return null; } if (result.size() > 1) { throw new ImageReadException("More than one XMP Block in GIF."); } return result.get(0); } finally { try { if (is != null) { is.close(); } } catch (final Exception e) { Debug.debug(e); } } } }