/* * Copyright (c) 2001, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.imageio.plugins.jpeg; import javax.imageio.IIOException; import javax.imageio.IIOImage; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageReader; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadata; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.imageio.event.IIOReadProgressListener; import java.awt.Graphics; import java.awt.color.ICC_Profile; import java.awt.color.ICC_ColorSpace; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.IndexColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.util.List; import java.util.ArrayList; import java.util.Iterator; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.NamedNodeMap; /** * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific) * marker segment. Inner classes are included for JFXX extension * marker segments, for different varieties of thumbnails, and for * ICC Profile APP2 marker segments. Any of these secondary types * that occur are kept as members of a single JFIFMarkerSegment object. */ class JFIFMarkerSegment extends MarkerSegment { int majorVersion; int minorVersion; int resUnits; int Xdensity; int Ydensity; int thumbWidth; int thumbHeight; JFIFThumbRGB thumb = null; // If present ArrayList extSegments = new ArrayList(); ICCMarkerSegment iccSegment = null; // optional ICC private static final int THUMB_JPEG = 0x10; private static final int THUMB_PALETTE = 0x11; private static final int THUMB_UNASSIGNED = 0x12; private static final int THUMB_RGB = 0x13; private static final int DATA_SIZE = 14; private static final int ID_SIZE = 5; private final int MAX_THUMB_WIDTH = 255; private final int MAX_THUMB_HEIGHT = 255; private final boolean debug = false; /** * Set to true when reading the chunks of an * ICC profile. All chunks are consolidated to create a single * "segment" containing all the chunks. This flag is a state * variable identifying whether to construct a new segment or * append to an old one. */ private boolean inICC = false; /** * A placeholder for an ICC profile marker segment under * construction. The segment is not added to the list * until all chunks have been read. */ private ICCMarkerSegment tempICCSegment = null; /** * Default constructor. Used to create a default JFIF header */ JFIFMarkerSegment() { super(JPEG.APP0); majorVersion = 1; minorVersion = 2; resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO; Xdensity = 1; Ydensity = 1; thumbWidth = 0; thumbHeight = 0; } /** * Constructs a JFIF header by reading from a stream wrapped * in a JPEGBuffer. */ JFIFMarkerSegment(JPEGBuffer buffer) throws IOException { super(buffer); buffer.bufPtr += ID_SIZE; // skip the id, we already checked it majorVersion = buffer.buf[buffer.bufPtr++]; minorVersion = buffer.buf[buffer.bufPtr++]; resUnits = buffer.buf[buffer.bufPtr++]; Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8; Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff; Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8; Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff; thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff; thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff; buffer.bufAvail -= DATA_SIZE; if (thumbWidth > 0) { thumb = new JFIFThumbRGB(buffer, thumbWidth, thumbHeight); } } /** * Constructs a JFIF header from a DOM Node. */ JFIFMarkerSegment(Node node) throws IIOInvalidTreeException { this(); updateFromNativeNode(node, true); } /** * Returns a deep-copy clone of this object. */ protected Object clone() { JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone(); if (!extSegments.isEmpty()) { // Clone the list with a deep copy newGuy.extSegments = new ArrayList(); for (Iterator iter = extSegments.iterator(); iter.hasNext();) { JFIFExtensionMarkerSegment jfxx = (JFIFExtensionMarkerSegment) iter.next(); newGuy.extSegments.add(jfxx.clone()); } } if (iccSegment != null) { newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone(); } return newGuy; } /** * Add an JFXX extension marker segment from the stream wrapped * in the JPEGBuffer to the list of extension segments. */ void addJFXX(JPEGBuffer buffer, JPEGImageReader reader) throws IOException { extSegments.add(new JFIFExtensionMarkerSegment(buffer, reader)); } /** * Adds an ICC Profile APP2 segment from the stream wrapped * in the JPEGBuffer. */ void addICC(JPEGBuffer buffer) throws IOException { if (inICC == false) { if (iccSegment != null) { throw new IIOException ("> 1 ICC APP2 Marker Segment not supported"); } tempICCSegment = new ICCMarkerSegment(buffer); if (inICC == false) { // Just one chunk iccSegment = tempICCSegment; tempICCSegment = null; } } else { if (tempICCSegment.addData(buffer) == true) { iccSegment = tempICCSegment; tempICCSegment = null; } } } /** * Add an ICC Profile APP2 segment by constructing it from * the given ICC_ColorSpace object. */ void addICC(ICC_ColorSpace cs) throws IOException { if (iccSegment != null) { throw new IIOException ("> 1 ICC APP2 Marker Segment not supported"); } iccSegment = new ICCMarkerSegment(cs); } /** * Returns a tree of DOM nodes representing this object and any * subordinate JFXX extension or ICC Profile segments. */ IIOMetadataNode getNativeNode() { IIOMetadataNode node = new IIOMetadataNode("app0JFIF"); node.setAttribute("majorVersion", Integer.toString(majorVersion)); node.setAttribute("minorVersion", Integer.toString(minorVersion)); node.setAttribute("resUnits", Integer.toString(resUnits)); node.setAttribute("Xdensity", Integer.toString(Xdensity)); node.setAttribute("Ydensity", Integer.toString(Ydensity)); node.setAttribute("thumbWidth", Integer.toString(thumbWidth)); node.setAttribute("thumbHeight", Integer.toString(thumbHeight)); if (!extSegments.isEmpty()) { IIOMetadataNode JFXXnode = new IIOMetadataNode("JFXX"); node.appendChild(JFXXnode); for (Iterator iter = extSegments.iterator(); iter.hasNext();) { JFIFExtensionMarkerSegment seg = (JFIFExtensionMarkerSegment) iter.next(); JFXXnode.appendChild(seg.getNativeNode()); } } if (iccSegment != null) { node.appendChild(iccSegment.getNativeNode()); } return node; } /** * Updates the data in this object from the given DOM Node tree. * If fromScratch is true, this object is being constructed. * Otherwise an existing object is being modified. * Throws an IIOInvalidTreeException if the tree is invalid in * any way. */ void updateFromNativeNode(Node node, boolean fromScratch) throws IIOInvalidTreeException { // none of the attributes are required NamedNodeMap attrs = node.getAttributes(); if (attrs.getLength() > 0) { int value = getAttributeValue(node, attrs, "majorVersion", 0, 255, false); majorVersion = (value != -1) ? value : majorVersion; value = getAttributeValue(node, attrs, "minorVersion", 0, 255, false); minorVersion = (value != -1) ? value : minorVersion; value = getAttributeValue(node, attrs, "resUnits", 0, 2, false); resUnits = (value != -1) ? value : resUnits; value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false); Xdensity = (value != -1) ? value : Xdensity; value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false); Ydensity = (value != -1) ? value : Ydensity; value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false); thumbWidth = (value != -1) ? value : thumbWidth; value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false); thumbHeight = (value != -1) ? value : thumbHeight; } if (node.hasChildNodes()) { NodeList children = node.getChildNodes(); int count = children.getLength(); if (count > 2) { throw new IIOInvalidTreeException ("app0JFIF node cannot have > 2 children", node); } for (int i = 0; i < count; i++) { Node child = children.item(i); String name = child.getNodeName(); if (name.equals("JFXX")) { if ((!extSegments.isEmpty()) && fromScratch) { throw new IIOInvalidTreeException ("app0JFIF node cannot have > 1 JFXX node", node); } NodeList exts = child.getChildNodes(); int extCount = exts.getLength(); for (int j = 0; j < extCount; j++) { Node ext = exts.item(j); extSegments.add(new JFIFExtensionMarkerSegment(ext)); } } if (name.equals("app2ICC")) { if ((iccSegment != null) && fromScratch) { throw new IIOInvalidTreeException ("> 1 ICC APP2 Marker Segment not supported", node); } iccSegment = new ICCMarkerSegment(child); } } } } int getThumbnailWidth(int index) { if (thumb != null) { if (index == 0) { return thumb.getWidth(); } index--; } JFIFExtensionMarkerSegment jfxx = (JFIFExtensionMarkerSegment) extSegments.get(index); return jfxx.thumb.getWidth(); } int getThumbnailHeight(int index) { if (thumb != null) { if (index == 0) { return thumb.getHeight(); } index--; } JFIFExtensionMarkerSegment jfxx = (JFIFExtensionMarkerSegment) extSegments.get(index); return jfxx.thumb.getHeight(); } BufferedImage getThumbnail(ImageInputStream iis, int index, JPEGImageReader reader) throws IOException { reader.thumbnailStarted(index); BufferedImage ret = null; if ((thumb != null) && (index == 0)) { ret = thumb.getThumbnail(iis, reader); } else { if (thumb != null) { index--; } JFIFExtensionMarkerSegment jfxx = (JFIFExtensionMarkerSegment) extSegments.get(index); ret = jfxx.thumb.getThumbnail(iis, reader); } reader.thumbnailComplete(); return ret; } /** * Writes the data for this segment to the stream in * valid JPEG format. Assumes that there will be no thumbnail. */ void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { // No thumbnail write(ios, null, writer); } /** * Writes the data for this segment to the stream in * valid JPEG format. The length written takes the thumbnail * width and height into account. If necessary, the thumbnail * is clipped to 255 x 255 and a warning is sent to the writer * argument. Progress updates are sent to the writer argument. */ void write(ImageOutputStream ios, BufferedImage thumb, JPEGImageWriter writer) throws IOException { int thumbWidth = 0; int thumbHeight = 0; int thumbLength = 0; int [] thumbData = null; if (thumb != null) { // Clip if necessary and get the data in thumbData thumbWidth = thumb.getWidth(); thumbHeight = thumb.getHeight(); if ((thumbWidth > MAX_THUMB_WIDTH) || (thumbHeight > MAX_THUMB_HEIGHT)) { writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED); } thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH); thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT); thumbData = thumb.getRaster().getPixels(0, 0, thumbWidth, thumbHeight, (int []) null); thumbLength = thumbData.length; } length = DATA_SIZE + LENGTH_SIZE + thumbLength; writeTag(ios); byte [] id = {0x4A, 0x46, 0x49, 0x46, 0x00}; ios.write(id); ios.write(majorVersion); ios.write(minorVersion); ios.write(resUnits); write2bytes(ios, Xdensity); write2bytes(ios, Ydensity); ios.write(thumbWidth); ios.write(thumbHeight); if (thumbData != null) { writer.thumbnailStarted(0); writeThumbnailData(ios, thumbData, writer); writer.thumbnailComplete(); } } /* * Write out the values in the integer array as a sequence of bytes, * reporting progress to the writer argument. */ void writeThumbnailData(ImageOutputStream ios, int [] thumbData, JPEGImageWriter writer) throws IOException { int progInterval = thumbData.length / 20; // approx. every 5% if (progInterval == 0) { progInterval = 1; } for (int i = 0; i < thumbData.length; i++) { ios.write(thumbData[i]); if ((i > progInterval) && (i % progInterval == 0)) { writer.thumbnailProgress (((float) i * 100) / ((float) thumbData.length)); } } } /** * Write out this JFIF Marker Segment, including a thumbnail or * appending a series of JFXX Marker Segments, as appropriate. * Warnings and progress reports are sent to the writer argument. * The list of thumbnails is matched to the list of JFXX extension * segments, if any, in order to determine how to encode the * thumbnails. If there are more thumbnails than metadata segments, * default encoding is used for the extra thumbnails. */ void writeWithThumbs(ImageOutputStream ios, List thumbnails, JPEGImageWriter writer) throws IOException { if (thumbnails != null) { JFIFExtensionMarkerSegment jfxx = null; if (thumbnails.size() == 1) { if (!extSegments.isEmpty()) { jfxx = (JFIFExtensionMarkerSegment) extSegments.get(0); } writeThumb(ios, (BufferedImage) thumbnails.get(0), jfxx, 0, true, writer); } else { // All others write as separate JFXX segments write(ios, writer); // Just the header without any thumbnail for (int i = 0; i < thumbnails.size(); i++) { jfxx = null; if (i < extSegments.size()) { jfxx = (JFIFExtensionMarkerSegment) extSegments.get(i); } writeThumb(ios, (BufferedImage) thumbnails.get(i), jfxx, i, false, writer); } } } else { // No thumbnails write(ios, writer); } } private void writeThumb(ImageOutputStream ios, BufferedImage thumb, JFIFExtensionMarkerSegment jfxx, int index, boolean onlyOne, JPEGImageWriter writer) throws IOException { ColorModel cm = thumb.getColorModel(); ColorSpace cs = cm.getColorSpace(); if (cm instanceof IndexColorModel) { // We never write a palette image into the header // So if it's the only one, we need to write the header first if (onlyOne) { write(ios, writer); } if ((jfxx == null) || (jfxx.code == THUMB_PALETTE)) { writeJFXXSegment(index, thumb, ios, writer); // default } else { // Expand to RGB BufferedImage thumbRGB = ((IndexColorModel) cm).convertToIntDiscrete (thumb.getRaster(), false); jfxx.setThumbnail(thumbRGB); writer.thumbnailStarted(index); jfxx.write(ios, writer); // Handles clipping if needed writer.thumbnailComplete(); } } else if (cs.getType() == ColorSpace.TYPE_RGB) { if (jfxx == null) { if (onlyOne) { write(ios, thumb, writer); // As part of the header } else { writeJFXXSegment(index, thumb, ios, writer); // default } } else { // If this is the only one, write the header first if (onlyOne) { write(ios, writer); } if (jfxx.code == THUMB_PALETTE) { writeJFXXSegment(index, thumb, ios, writer); // default writer.warningOccurred (JPEGImageWriter.WARNING_NO_RGB_THUMB_AS_INDEXED); } else { jfxx.setThumbnail(thumb); writer.thumbnailStarted(index); jfxx.write(ios, writer); // Handles clipping if needed writer.thumbnailComplete(); } } } else if (cs.getType() == ColorSpace.TYPE_GRAY) { if (jfxx == null) { if (onlyOne) { BufferedImage thumbRGB = expandGrayThumb(thumb); write(ios, thumbRGB, writer); // As part of the header } else { writeJFXXSegment(index, thumb, ios, writer); // default } } else { // If this is the only one, write the header first if (onlyOne) { write(ios, writer); } if (jfxx.code == THUMB_RGB) { BufferedImage thumbRGB = expandGrayThumb(thumb); writeJFXXSegment(index, thumbRGB, ios, writer); } else if (jfxx.code == THUMB_JPEG) { jfxx.setThumbnail(thumb); writer.thumbnailStarted(index); jfxx.write(ios, writer); // Handles clipping if needed writer.thumbnailComplete(); } else if (jfxx.code == THUMB_PALETTE) { writeJFXXSegment(index, thumb, ios, writer); // default writer.warningOccurred (JPEGImageWriter.WARNING_NO_GRAY_THUMB_AS_INDEXED); } } } else { writer.warningOccurred (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL); } } // Could put reason codes in here to be parsed in writeJFXXSegment // in order to provide more meaningful warnings. private class IllegalThumbException extends Exception {} /** * Writes out a new JFXX extension segment, without saving it. */ private void writeJFXXSegment(int index, BufferedImage thumbnail, ImageOutputStream ios, JPEGImageWriter writer) throws IOException { JFIFExtensionMarkerSegment jfxx = null; try { jfxx = new JFIFExtensionMarkerSegment(thumbnail); } catch (IllegalThumbException e) { writer.warningOccurred (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL); return; } writer.thumbnailStarted(index); jfxx.write(ios, writer); writer.thumbnailComplete(); } /** * Return an RGB image that is the expansion of the given grayscale * image. */ private static BufferedImage expandGrayThumb(BufferedImage thumb) { BufferedImage ret = new BufferedImage(thumb.getWidth(), thumb.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics g = ret.getGraphics(); g.drawImage(thumb, 0, 0, null); return ret; } /** * Writes out a default JFIF marker segment to the given * output stream. If thumbnails is not null, * writes out the set of thumbnail images as JFXX marker segments, or * incorporated into the JFIF segment if appropriate. * If iccProfile is not null, * writes out the profile after the JFIF segment using as many APP2 * marker segments as necessary. */ static void writeDefaultJFIF(ImageOutputStream ios, List thumbnails, ICC_Profile iccProfile, JPEGImageWriter writer) throws IOException { JFIFMarkerSegment jfif = new JFIFMarkerSegment(); jfif.writeWithThumbs(ios, thumbnails, writer); if (iccProfile != null) { writeICC(iccProfile, ios); } } /** * Prints out the contents of this object to System.out for debugging. */ void print() { printTag("JFIF"); System.out.print("Version "); System.out.print(majorVersion); System.out.println(".0" + Integer.toString(minorVersion)); System.out.print("Resolution units: "); System.out.println(resUnits); System.out.print("X density: "); System.out.println(Xdensity); System.out.print("Y density: "); System.out.println(Ydensity); System.out.print("Thumbnail Width: "); System.out.println(thumbWidth); System.out.print("Thumbnail Height: "); System.out.println(thumbHeight); if (!extSegments.isEmpty()) { for (Iterator iter = extSegments.iterator(); iter.hasNext();) { JFIFExtensionMarkerSegment extSegment = (JFIFExtensionMarkerSegment) iter.next(); extSegment.print(); } } if (iccSegment != null) { iccSegment.print(); } } /** * A JFIF extension APP0 marker segment. */ class JFIFExtensionMarkerSegment extends MarkerSegment { int code; JFIFThumb thumb; private static final int DATA_SIZE = 6; private static final int ID_SIZE = 5; JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader) throws IOException { super(buffer); buffer.bufPtr += ID_SIZE; // skip the id, we already checked it code = buffer.buf[buffer.bufPtr++] & 0xff; buffer.bufAvail -= DATA_SIZE; if (code == THUMB_JPEG) { thumb = new JFIFThumbJPEG(buffer, length, reader); } else { buffer.loadBuf(2); int thumbX = buffer.buf[buffer.bufPtr++] & 0xff; int thumbY = buffer.buf[buffer.bufPtr++] & 0xff; buffer.bufAvail -= 2; // following constructors handle bufAvail if (code == THUMB_PALETTE) { thumb = new JFIFThumbPalette(buffer, thumbX, thumbY); } else { thumb = new JFIFThumbRGB(buffer, thumbX, thumbY); } } } JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException { super(JPEG.APP0); NamedNodeMap attrs = node.getAttributes(); if (attrs.getLength() > 0) { code = getAttributeValue(node, attrs, "extensionCode", THUMB_JPEG, THUMB_RGB, false); if (code == THUMB_UNASSIGNED) { throw new IIOInvalidTreeException ("invalid extensionCode attribute value", node); } } else { code = THUMB_UNASSIGNED; } // Now the child if (node.getChildNodes().getLength() != 1) { throw new IIOInvalidTreeException ("app0JFXX node must have exactly 1 child", node); } Node child = node.getFirstChild(); String name = child.getNodeName(); if (name.equals("JFIFthumbJPEG")) { if (code == THUMB_UNASSIGNED) { code = THUMB_JPEG; } thumb = new JFIFThumbJPEG(child); } else if (name.equals("JFIFthumbPalette")) { if (code == THUMB_UNASSIGNED) { code = THUMB_PALETTE; } thumb = new JFIFThumbPalette(child); } else if (name.equals("JFIFthumbRGB")) { if (code == THUMB_UNASSIGNED) { code = THUMB_RGB; } thumb = new JFIFThumbRGB(child); } else { throw new IIOInvalidTreeException ("unrecognized app0JFXX child node", node); } } JFIFExtensionMarkerSegment(BufferedImage thumbnail) throws IllegalThumbException { super(JPEG.APP0); ColorModel cm = thumbnail.getColorModel(); int csType = cm.getColorSpace().getType(); if (cm.hasAlpha()) { throw new IllegalThumbException(); } if (cm instanceof IndexColorModel) { code = THUMB_PALETTE; thumb = new JFIFThumbPalette(thumbnail); } else if (csType == ColorSpace.TYPE_RGB) { code = THUMB_RGB; thumb = new JFIFThumbRGB(thumbnail); } else if (csType == ColorSpace.TYPE_GRAY) { code = THUMB_JPEG; thumb = new JFIFThumbJPEG(thumbnail); } else { throw new IllegalThumbException(); } } void setThumbnail(BufferedImage thumbnail) { try { switch (code) { case THUMB_PALETTE: thumb = new JFIFThumbPalette(thumbnail); break; case THUMB_RGB: thumb = new JFIFThumbRGB(thumbnail); break; case THUMB_JPEG: thumb = new JFIFThumbJPEG(thumbnail); break; } } catch (IllegalThumbException e) { // Should never happen throw new InternalError("Illegal thumb in setThumbnail!", e); } } protected Object clone() { JFIFExtensionMarkerSegment newGuy = (JFIFExtensionMarkerSegment) super.clone(); if (thumb != null) { newGuy.thumb = (JFIFThumb) thumb.clone(); } return newGuy; } IIOMetadataNode getNativeNode() { IIOMetadataNode node = new IIOMetadataNode("app0JFXX"); node.setAttribute("extensionCode", Integer.toString(code)); node.appendChild(thumb.getNativeNode()); return node; } void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { length = LENGTH_SIZE + DATA_SIZE + thumb.getLength(); writeTag(ios); byte [] id = {0x4A, 0x46, 0x58, 0x58, 0x00}; ios.write(id); ios.write(code); thumb.write(ios, writer); } void print() { printTag("JFXX"); thumb.print(); } } /** * A superclass for the varieties of thumbnails that can * be stored in a JFIF extension marker segment. */ abstract class JFIFThumb implements Cloneable { long streamPos = -1L; // Save the thumbnail pos when reading abstract int getLength(); // When writing abstract int getWidth(); abstract int getHeight(); abstract BufferedImage getThumbnail(ImageInputStream iis, JPEGImageReader reader) throws IOException; protected JFIFThumb() {} protected JFIFThumb(JPEGBuffer buffer) throws IOException{ // Save the stream position for reading the thumbnail later streamPos = buffer.getStreamPosition(); } abstract void print(); abstract IIOMetadataNode getNativeNode(); abstract void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException; protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) {} // won't happen return null; } } abstract class JFIFThumbUncompressed extends JFIFThumb { BufferedImage thumbnail = null; int thumbWidth; int thumbHeight; String name; JFIFThumbUncompressed(JPEGBuffer buffer, int width, int height, int skip, String name) throws IOException { super(buffer); thumbWidth = width; thumbHeight = height; // Now skip the thumbnail data buffer.skipData(skip); this.name = name; } JFIFThumbUncompressed(Node node, String name) throws IIOInvalidTreeException { thumbWidth = 0; thumbHeight = 0; this.name = name; NamedNodeMap attrs = node.getAttributes(); int count = attrs.getLength(); if (count > 2) { throw new IIOInvalidTreeException (name +" node cannot have > 2 attributes", node); } if (count != 0) { int value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false); thumbWidth = (value != -1) ? value : thumbWidth; value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false); thumbHeight = (value != -1) ? value : thumbHeight; } } JFIFThumbUncompressed(BufferedImage thumb) { thumbnail = thumb; thumbWidth = thumb.getWidth(); thumbHeight = thumb.getHeight(); name = null; // not used when writing } void readByteBuffer(ImageInputStream iis, byte [] data, JPEGImageReader reader, float workPortion, float workOffset) throws IOException { int progInterval = Math.max((int)(data.length/20/workPortion), 1); for (int offset = 0; offset < data.length;) { int len = Math.min(progInterval, data.length-offset); iis.read(data, offset, len); offset += progInterval; float percentDone = ((float) offset* 100) / data.length * workPortion + workOffset; if (percentDone > 100.0F) { percentDone = 100.0F; } reader.thumbnailProgress (percentDone); } } int getWidth() { return thumbWidth; } int getHeight() { return thumbHeight; } IIOMetadataNode getNativeNode() { IIOMetadataNode node = new IIOMetadataNode(name); node.setAttribute("thumbWidth", Integer.toString(thumbWidth)); node.setAttribute("thumbHeight", Integer.toString(thumbHeight)); return node; } void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { if ((thumbWidth > MAX_THUMB_WIDTH) || (thumbHeight > MAX_THUMB_HEIGHT)) { writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED); } thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH); thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT); ios.write(thumbWidth); ios.write(thumbHeight); } void writePixels(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { if ((thumbWidth > MAX_THUMB_WIDTH) || (thumbHeight > MAX_THUMB_HEIGHT)) { writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED); } thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH); thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT); int [] data = thumbnail.getRaster().getPixels(0, 0, thumbWidth, thumbHeight, (int []) null); writeThumbnailData(ios, data, writer); } void print() { System.out.print(name + " width: "); System.out.println(thumbWidth); System.out.print(name + " height: "); System.out.println(thumbHeight); } } /** * A JFIF thumbnail stored as RGB, one byte per channel, * interleaved. */ class JFIFThumbRGB extends JFIFThumbUncompressed { JFIFThumbRGB(JPEGBuffer buffer, int width, int height) throws IOException { super(buffer, width, height, width*height*3, "JFIFthumbRGB"); } JFIFThumbRGB(Node node) throws IIOInvalidTreeException { super(node, "JFIFthumbRGB"); } JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException { super(thumb); } int getLength() { return (thumbWidth*thumbHeight*3); } BufferedImage getThumbnail(ImageInputStream iis, JPEGImageReader reader) throws IOException { iis.mark(); iis.seek(streamPos); DataBufferByte buffer = new DataBufferByte(getLength()); readByteBuffer(iis, buffer.getData(), reader, 1.0F, 0.0F); iis.reset(); WritableRaster raster = Raster.createInterleavedRaster(buffer, thumbWidth, thumbHeight, thumbWidth*3, 3, new int [] {0, 1, 2}, null); ColorModel cm = new ComponentColorModel(JPEG.JCS.sRGB, false, false, ColorModel.OPAQUE, DataBuffer.TYPE_BYTE); return new BufferedImage(cm, raster, false, null); } void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { super.write(ios, writer); // width and height writePixels(ios, writer); } } /** * A JFIF thumbnail stored as an indexed palette image * using an RGB palette. */ class JFIFThumbPalette extends JFIFThumbUncompressed { private static final int PALETTE_SIZE = 768; JFIFThumbPalette(JPEGBuffer buffer, int width, int height) throws IOException { super(buffer, width, height, PALETTE_SIZE + width * height, "JFIFThumbPalette"); } JFIFThumbPalette(Node node) throws IIOInvalidTreeException { super(node, "JFIFThumbPalette"); } JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException { super(thumb); IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel(); if (icm.getMapSize() > 256) { throw new IllegalThumbException(); } } int getLength() { return (thumbWidth*thumbHeight + PALETTE_SIZE); } BufferedImage getThumbnail(ImageInputStream iis, JPEGImageReader reader) throws IOException { iis.mark(); iis.seek(streamPos); // read the palette byte [] palette = new byte [PALETTE_SIZE]; float palettePart = ((float) PALETTE_SIZE) / getLength(); readByteBuffer(iis, palette, reader, palettePart, 0.0F); DataBufferByte buffer = new DataBufferByte(thumbWidth*thumbHeight); readByteBuffer(iis, buffer.getData(), reader, 1.0F-palettePart, palettePart); iis.read(); iis.reset(); IndexColorModel cm = new IndexColorModel(8, 256, palette, 0, false); SampleModel sm = cm.createCompatibleSampleModel(thumbWidth, thumbHeight); WritableRaster raster = Raster.createWritableRaster(sm, buffer, null); return new BufferedImage(cm, raster, false, null); } void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { super.write(ios, writer); // width and height // Write the palette (must be 768 bytes) byte [] palette = new byte[768]; IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel(); byte [] reds = new byte [256]; byte [] greens = new byte [256]; byte [] blues = new byte [256]; icm.getReds(reds); icm.getGreens(greens); icm.getBlues(blues); for (int i = 0; i < 256; i++) { palette[i*3] = reds[i]; palette[i*3+1] = greens[i]; palette[i*3+2] = blues[i]; } ios.write(palette); writePixels(ios, writer); } } /** * A JFIF thumbnail stored as a JPEG stream. No JFIF or * JFIF extension markers are permitted. There is no need * to clip these, but the entire image must fit into a * single JFXX marker segment. */ class JFIFThumbJPEG extends JFIFThumb { JPEGMetadata thumbMetadata = null; byte [] data = null; // Compressed image data, for writing private static final int PREAMBLE_SIZE = 6; JFIFThumbJPEG(JPEGBuffer buffer, int length, JPEGImageReader reader) throws IOException { super(buffer); // Compute the final stream position long finalPos = streamPos + (length - PREAMBLE_SIZE); // Set the stream back to the start of the thumbnail // and read its metadata (but don't decode the image) buffer.iis.seek(streamPos); thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader); // Set the stream to the computed final position buffer.iis.seek(finalPos); // Clear the now invalid buffer buffer.bufAvail = 0; buffer.bufPtr = 0; } JFIFThumbJPEG(Node node) throws IIOInvalidTreeException { if (node.getChildNodes().getLength() > 1) { throw new IIOInvalidTreeException ("JFIFThumbJPEG node must have 0 or 1 child", node); } Node child = node.getFirstChild(); if (child != null) { String name = child.getNodeName(); if (!name.equals("markerSequence")) { throw new IIOInvalidTreeException ("JFIFThumbJPEG child must be a markerSequence node", node); } thumbMetadata = new JPEGMetadata(false, true); thumbMetadata.setFromMarkerSequenceNode(child); } } JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException { int INITIAL_BUFSIZE = 4096; int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(INITIAL_BUFSIZE); MemoryCacheImageOutputStream mos = new MemoryCacheImageOutputStream(baos); JPEGImageWriter thumbWriter = new JPEGImageWriter(null); thumbWriter.setOutput(mos); // get default metadata for the thumb JPEGMetadata metadata = (JPEGMetadata) thumbWriter.getDefaultImageMetadata (new ImageTypeSpecifier(thumb), null); // Remove the jfif segment, which should be there. MarkerSegment jfif = metadata.findMarkerSegment (JFIFMarkerSegment.class, true); if (jfif == null) { throw new IllegalThumbException(); } metadata.markerSequence.remove(jfif); /* Use this if removing leaves a hole and causes trouble // Get the tree String format = metadata.getNativeMetadataFormatName(); IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); // If there is no app0jfif node, the image is bad NodeList jfifs = tree.getElementsByTagName("app0JFIF"); if (jfifs.getLength() == 0) { throw new IllegalThumbException(); } // remove the app0jfif node Node jfif = jfifs.item(0); Node parent = jfif.getParentNode(); parent.removeChild(jfif); metadata.setFromTree(format, tree); */ thumbWriter.write(new IIOImage(thumb, null, metadata)); thumbWriter.dispose(); // Now check that the size is OK if (baos.size() > MAZ_BUFSIZE) { throw new IllegalThumbException(); } data = baos.toByteArray(); } catch (IOException e) { throw new IllegalThumbException(); } } int getWidth() { int retval = 0; SOFMarkerSegment sof = (SOFMarkerSegment) thumbMetadata.findMarkerSegment (SOFMarkerSegment.class, true); if (sof != null) { retval = sof.samplesPerLine; } return retval; } int getHeight() { int retval = 0; SOFMarkerSegment sof = (SOFMarkerSegment) thumbMetadata.findMarkerSegment (SOFMarkerSegment.class, true); if (sof != null) { retval = sof.numLines; } return retval; } private class ThumbnailReadListener implements IIOReadProgressListener { JPEGImageReader reader = null; ThumbnailReadListener (JPEGImageReader reader) { this.reader = reader; } public void sequenceStarted(ImageReader source, int minIndex) {} public void sequenceComplete(ImageReader source) {} public void imageStarted(ImageReader source, int imageIndex) {} public void imageProgress(ImageReader source, float percentageDone) { reader.thumbnailProgress(percentageDone); } public void imageComplete(ImageReader source) {} public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {} public void thumbnailProgress(ImageReader source, float percentageDone) {} public void thumbnailComplete(ImageReader source) {} public void readAborted(ImageReader source) {} } BufferedImage getThumbnail(ImageInputStream iis, JPEGImageReader reader) throws IOException { iis.mark(); iis.seek(streamPos); JPEGImageReader thumbReader = new JPEGImageReader(null); thumbReader.setInput(iis); thumbReader.addIIOReadProgressListener (new ThumbnailReadListener(reader)); BufferedImage ret = thumbReader.read(0, null); thumbReader.dispose(); iis.reset(); return ret; } protected Object clone() { JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone(); if (thumbMetadata != null) { newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone(); } return newGuy; } IIOMetadataNode getNativeNode() { IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG"); if (thumbMetadata != null) { node.appendChild(thumbMetadata.getNativeTree()); } return node; } int getLength() { if (data == null) { return 0; } else { return data.length; } } void write(ImageOutputStream ios, JPEGImageWriter writer) throws IOException { int progInterval = data.length / 20; // approx. every 5% if (progInterval == 0) { progInterval = 1; } for (int offset = 0; offset < data.length;) { int len = Math.min(progInterval, data.length-offset); ios.write(data, offset, len); offset += progInterval; float percentDone = ((float) offset * 100) / data.length; if (percentDone > 100.0F) { percentDone = 100.0F; } writer.thumbnailProgress (percentDone); } } void print () { System.out.println("JFIF thumbnail stored as JPEG"); } } /** * Write out the given profile to the stream, embedded in * the necessary number of APP2 segments, per the ICC spec. * This is the only mechanism for writing an ICC profile * to a stream. */ static void writeICC(ICC_Profile profile, ImageOutputStream ios) throws IOException { int LENGTH_LENGTH = 2; final String ID = "ICC_PROFILE"; int ID_LENGTH = ID.length()+1; // spec says it's null-terminated int COUNTS_LENGTH = 2; int MAX_ICC_CHUNK_SIZE = 65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH; byte [] data = profile.getData(); int numChunks = data.length / MAX_ICC_CHUNK_SIZE; if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) { numChunks++; } int chunkNum = 1; int offset = 0; for (int i = 0; i < numChunks; i++) { int dataLength = Math.min(data.length-offset, MAX_ICC_CHUNK_SIZE); int segLength = dataLength+COUNTS_LENGTH+ID_LENGTH+LENGTH_LENGTH; ios.write(0xff); ios.write(JPEG.APP2); MarkerSegment.write2bytes(ios, segLength); byte [] id = ID.getBytes("US-ASCII"); ios.write(id); ios.write(0); // Null-terminate the string ios.write(chunkNum++); ios.write(numChunks); ios.write(data, offset, dataLength); offset += dataLength; } } /** * An APP2 marker segment containing an ICC profile. In the stream * a profile larger than 64K is broken up into a series of chunks. * This inner class represents the complete profile as a single object, * combining chunks as necessary. */ class ICCMarkerSegment extends MarkerSegment { ArrayList chunks = null; byte [] profile = null; // The complete profile when it's fully read // May remain null when writing private static final int ID_SIZE = 12; int chunksRead; int numChunks; ICCMarkerSegment(ICC_ColorSpace cs) { super(JPEG.APP2); chunks = null; chunksRead = 0; numChunks = 0; profile = cs.getProfile().getData(); } ICCMarkerSegment(JPEGBuffer buffer) throws IOException { super(buffer); // gets whole segment or fills the buffer if (debug) { System.out.println("Creating new ICC segment"); } buffer.bufPtr += ID_SIZE; // Skip the id buffer.bufAvail -= ID_SIZE; /* * Reduce the stored length by the id size. The stored * length is used to store the length of the profile * data only. */ length -= ID_SIZE; // get the chunk number int chunkNum = buffer.buf[buffer.bufPtr] & 0xff; // get the total number of chunks numChunks = buffer.buf[buffer.bufPtr+1] & 0xff; if (chunkNum > numChunks) { throw new IIOException ("Image format Error; chunk num > num chunks"); } // if there are no more chunks, set up the data if (numChunks == 1) { // reduce the stored length by the two chunk numbering bytes length -= 2; profile = new byte[length]; buffer.bufPtr += 2; buffer.bufAvail-=2; buffer.readData(profile); inICC = false; } else { // If we store them away, include the chunk numbering bytes byte [] profileData = new byte[length]; // Now reduce the stored length by the // two chunk numbering bytes length -= 2; buffer.readData(profileData); chunks = new ArrayList(); chunks.add(profileData); chunksRead = 1; inICC = true; } } ICCMarkerSegment(Node node) throws IIOInvalidTreeException { super(JPEG.APP2); if (node instanceof IIOMetadataNode) { IIOMetadataNode ourNode = (IIOMetadataNode) node; ICC_Profile prof = (ICC_Profile) ourNode.getUserObject(); if (prof != null) { // May be null profile = prof.getData(); } } } protected Object clone () { ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone(); if (profile != null) { newGuy.profile = profile.clone(); } return newGuy; } boolean addData(JPEGBuffer buffer) throws IOException { if (debug) { System.out.println("Adding to ICC segment"); } // skip the tag buffer.bufPtr++; buffer.bufAvail--; // Get the length, but not in length int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8; dataLen |= buffer.buf[buffer.bufPtr++] & 0xff; buffer.bufAvail -= 2; // Don't include length itself dataLen -= 2; // skip the id buffer.bufPtr += ID_SIZE; // Skip the id buffer.bufAvail -= ID_SIZE; /* * Reduce the stored length by the id size. The stored * length is used to store the length of the profile * data only. */ dataLen -= ID_SIZE; // get the chunk number int chunkNum = buffer.buf[buffer.bufPtr] & 0xff; if (chunkNum > numChunks) { throw new IIOException ("Image format Error; chunk num > num chunks"); } // get the number of chunks, which should match int newNumChunks = buffer.buf[buffer.bufPtr+1] & 0xff; if (numChunks != newNumChunks) { throw new IIOException ("Image format Error; icc num chunks mismatch"); } dataLen -= 2; if (debug) { System.out.println("chunkNum: " + chunkNum + ", numChunks: " + numChunks + ", dataLen: " + dataLen); } boolean retval = false; byte [] profileData = new byte[dataLen]; buffer.readData(profileData); chunks.add(profileData); length += dataLen; chunksRead++; if (chunksRead < numChunks) { inICC = true; } else { if (debug) { System.out.println("Completing profile; total length is " + length); } // create an array for the whole thing profile = new byte[length]; // copy the existing chunks, releasing them // Note that they may be out of order int index = 0; for (int i = 1; i <= numChunks; i++) { boolean foundIt = false; for (int chunk = 0; chunk < chunks.size(); chunk++) { byte [] chunkData = (byte []) chunks.get(chunk); if (chunkData[0] == i) { // Right one System.arraycopy(chunkData, 2, profile, index, chunkData.length-2); index += chunkData.length-2; foundIt = true; } } if (foundIt == false) { throw new IIOException ("Image Format Error: Missing ICC chunk num " + i); } } chunks = null; chunksRead = 0; numChunks = 0; inICC = false; retval = true; } return retval; } IIOMetadataNode getNativeNode() { IIOMetadataNode node = new IIOMetadataNode("app2ICC"); if (profile != null) { node.setUserObject(ICC_Profile.getInstance(profile)); } return node; } /** * No-op. Profiles are never written from metadata. * They are written from the ColorSpace of the image. */ void write(ImageOutputStream ios) throws IOException { // No-op } void print () { printTag("ICC Profile APP2"); } } }