| /* |
| * Copyright (c) 2001, 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 <code>true</code> 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 <code>thumbnails</code> is not <code>null</code>, |
| * writes out the set of thumbnail images as JFXX marker segments, or |
| * incorporated into the JFIF segment if appropriate. |
| * If <code>iccProfile</code> is not <code>null</code>, |
| * 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!"); |
| } |
| } |
| |
| 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 objec, |
| * 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 = (byte[]) 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"); |
| } |
| } |
| } |