1 /*
   2  * Copyright (c) 2001, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.imageio.plugins.jpeg;
  27 
  28 import javax.imageio.IIOException;
  29 import javax.imageio.IIOImage;
  30 import javax.imageio.ImageTypeSpecifier;
  31 import javax.imageio.ImageReader;
  32 import javax.imageio.metadata.IIOInvalidTreeException;
  33 import javax.imageio.metadata.IIOMetadataNode;
  34 import javax.imageio.metadata.IIOMetadata;
  35 import javax.imageio.stream.ImageInputStream;
  36 import javax.imageio.stream.ImageOutputStream;
  37 import javax.imageio.stream.MemoryCacheImageOutputStream;
  38 import javax.imageio.event.IIOReadProgressListener;
  39 
  40 import java.awt.Graphics;
  41 import java.awt.color.ICC_Profile;
  42 import java.awt.color.ICC_ColorSpace;
  43 import java.awt.color.ColorSpace;
  44 import java.awt.image.ColorModel;
  45 import java.awt.image.SampleModel;
  46 import java.awt.image.IndexColorModel;
  47 import java.awt.image.ComponentColorModel;
  48 import java.awt.image.BufferedImage;
  49 import java.awt.image.DataBuffer;
  50 import java.awt.image.DataBufferByte;
  51 import java.awt.image.Raster;
  52 import java.awt.image.WritableRaster;
  53 import java.io.IOException;
  54 import java.io.ByteArrayOutputStream;
  55 import java.util.List;
  56 import java.util.ArrayList;
  57 import java.util.Iterator;
  58 
  59 import org.w3c.dom.Node;
  60 import org.w3c.dom.NodeList;
  61 import org.w3c.dom.NamedNodeMap;
  62 
  63 /**
  64  * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific)
  65  * marker segment.  Inner classes are included for JFXX extension
  66  * marker segments, for different varieties of thumbnails, and for
  67  * ICC Profile APP2 marker segments.  Any of these secondary types
  68  * that occur are kept as members of a single JFIFMarkerSegment object.
  69  */
  70 class JFIFMarkerSegment extends MarkerSegment {
  71     int majorVersion;
  72     int minorVersion;
  73     int resUnits;
  74     int Xdensity;
  75     int Ydensity;
  76     int thumbWidth;
  77     int thumbHeight;
  78     JFIFThumbRGB thumb = null;  // If present
  79     ArrayList extSegments = new ArrayList();
  80     ICCMarkerSegment iccSegment = null; // optional ICC
  81     private static final int THUMB_JPEG = 0x10;
  82     private static final int THUMB_PALETTE = 0x11;
  83     private static final int THUMB_UNASSIGNED = 0x12;
  84     private static final int THUMB_RGB = 0x13;
  85     private static final int DATA_SIZE = 14;
  86     private static final int ID_SIZE = 5;
  87     private final int MAX_THUMB_WIDTH = 255;
  88     private final int MAX_THUMB_HEIGHT = 255;
  89 
  90     private final boolean debug = false;
  91 
  92     /**
  93      * Set to <code>true</code> when reading the chunks of an
  94      * ICC profile.  All chunks are consolidated to create a single
  95      * "segment" containing all the chunks.  This flag is a state
  96      * variable identifying whether to construct a new segment or
  97      * append to an old one.
  98      */
  99     private boolean inICC = false;
 100 
 101     /**
 102      * A placeholder for an ICC profile marker segment under
 103      * construction.  The segment is not added to the list
 104      * until all chunks have been read.
 105      */
 106     private ICCMarkerSegment tempICCSegment = null;
 107 
 108 
 109     /**
 110      * Default constructor.  Used to create a default JFIF header
 111      */
 112     JFIFMarkerSegment() {
 113         super(JPEG.APP0);
 114         majorVersion = 1;
 115         minorVersion = 2;
 116         resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
 117         Xdensity = 1;
 118         Ydensity = 1;
 119         thumbWidth = 0;
 120         thumbHeight = 0;
 121     }
 122 
 123     /**
 124      * Constructs a JFIF header by reading from a stream wrapped
 125      * in a JPEGBuffer.
 126      */
 127     JFIFMarkerSegment(JPEGBuffer buffer) throws IOException {
 128         super(buffer);
 129         buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it
 130 
 131         majorVersion = buffer.buf[buffer.bufPtr++];
 132         minorVersion = buffer.buf[buffer.bufPtr++];
 133         resUnits = buffer.buf[buffer.bufPtr++];
 134         Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
 135         Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff;
 136         Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
 137         Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff;
 138         thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff;
 139         thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff;
 140         buffer.bufAvail -= DATA_SIZE;
 141         if (thumbWidth > 0) {
 142             thumb = new JFIFThumbRGB(buffer, thumbWidth, thumbHeight);
 143         }
 144     }
 145 
 146     /**
 147      * Constructs a JFIF header from a DOM Node.
 148      */
 149     JFIFMarkerSegment(Node node) throws IIOInvalidTreeException {
 150         this();
 151         updateFromNativeNode(node, true);
 152     }
 153 
 154     /**
 155      * Returns a deep-copy clone of this object.
 156      */
 157     protected Object clone() {
 158         JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone();
 159         if (!extSegments.isEmpty()) { // Clone the list with a deep copy
 160             newGuy.extSegments = new ArrayList();
 161             for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
 162                 JFIFExtensionMarkerSegment jfxx =
 163                     (JFIFExtensionMarkerSegment) iter.next();
 164                 newGuy.extSegments.add(jfxx.clone());
 165             }
 166         }
 167         if (iccSegment != null) {
 168             newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone();
 169         }
 170         return newGuy;
 171     }
 172 
 173     /**
 174      * Add an JFXX extension marker segment from the stream wrapped
 175      * in the JPEGBuffer to the list of extension segments.
 176      */
 177     void addJFXX(JPEGBuffer buffer, JPEGImageReader reader)
 178         throws IOException {
 179         extSegments.add(new JFIFExtensionMarkerSegment(buffer, reader));
 180     }
 181 
 182     /**
 183      * Adds an ICC Profile APP2 segment from the stream wrapped
 184      * in the JPEGBuffer.
 185      */
 186     void addICC(JPEGBuffer buffer) throws IOException {
 187         if (inICC == false) {
 188             if (iccSegment != null) {
 189                 throw new IIOException
 190                     ("> 1 ICC APP2 Marker Segment not supported");
 191             }
 192             tempICCSegment = new ICCMarkerSegment(buffer);
 193             if (inICC == false) { // Just one chunk
 194                 iccSegment = tempICCSegment;
 195                 tempICCSegment = null;
 196             }
 197         } else {
 198             if (tempICCSegment.addData(buffer) == true) {
 199                 iccSegment = tempICCSegment;
 200                 tempICCSegment = null;
 201             }
 202         }
 203     }
 204 
 205     /**
 206      * Add an ICC Profile APP2 segment by constructing it from
 207      * the given ICC_ColorSpace object.
 208      */
 209     void addICC(ICC_ColorSpace cs) throws IOException {
 210         if (iccSegment != null) {
 211             throw new IIOException
 212                 ("> 1 ICC APP2 Marker Segment not supported");
 213         }
 214         iccSegment = new ICCMarkerSegment(cs);
 215     }
 216 
 217     /**
 218      * Returns a tree of DOM nodes representing this object and any
 219      * subordinate JFXX extension or ICC Profile segments.
 220      */
 221     IIOMetadataNode getNativeNode() {
 222         IIOMetadataNode node = new IIOMetadataNode("app0JFIF");
 223         node.setAttribute("majorVersion", Integer.toString(majorVersion));
 224         node.setAttribute("minorVersion", Integer.toString(minorVersion));
 225         node.setAttribute("resUnits", Integer.toString(resUnits));
 226         node.setAttribute("Xdensity", Integer.toString(Xdensity));
 227         node.setAttribute("Ydensity", Integer.toString(Ydensity));
 228         node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
 229         node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
 230         if (!extSegments.isEmpty()) {
 231             IIOMetadataNode JFXXnode = new IIOMetadataNode("JFXX");
 232             node.appendChild(JFXXnode);
 233             for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
 234                 JFIFExtensionMarkerSegment seg =
 235                     (JFIFExtensionMarkerSegment) iter.next();
 236                 JFXXnode.appendChild(seg.getNativeNode());
 237             }
 238         }
 239         if (iccSegment != null) {
 240             node.appendChild(iccSegment.getNativeNode());
 241         }
 242 
 243         return node;
 244     }
 245 
 246     /**
 247      * Updates the data in this object from the given DOM Node tree.
 248      * If fromScratch is true, this object is being constructed.
 249      * Otherwise an existing object is being modified.
 250      * Throws an IIOInvalidTreeException if the tree is invalid in
 251      * any way.
 252      */
 253     void updateFromNativeNode(Node node, boolean fromScratch)
 254         throws IIOInvalidTreeException {
 255         // none of the attributes are required
 256         NamedNodeMap attrs = node.getAttributes();
 257         if (attrs.getLength() > 0) {
 258             int value = getAttributeValue(node, attrs, "majorVersion",
 259                                           0, 255, false);
 260             majorVersion = (value != -1) ? value : majorVersion;
 261             value = getAttributeValue(node, attrs, "minorVersion",
 262                                       0, 255, false);
 263             minorVersion = (value != -1) ? value : minorVersion;
 264             value = getAttributeValue(node, attrs, "resUnits", 0, 2, false);
 265             resUnits = (value != -1) ? value : resUnits;
 266             value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false);
 267             Xdensity = (value != -1) ? value : Xdensity;
 268             value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false);
 269             Ydensity = (value != -1) ? value : Ydensity;
 270             value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false);
 271             thumbWidth = (value != -1) ? value : thumbWidth;
 272             value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false);
 273             thumbHeight = (value != -1) ? value : thumbHeight;
 274         }
 275         if (node.hasChildNodes()) {
 276             NodeList children = node.getChildNodes();
 277             int count = children.getLength();
 278             if (count > 2) {
 279                 throw new IIOInvalidTreeException
 280                     ("app0JFIF node cannot have > 2 children", node);
 281             }
 282             for (int i = 0; i < count; i++) {
 283                 Node child = children.item(i);
 284                 String name = child.getNodeName();
 285                 if (name.equals("JFXX")) {
 286                     if ((!extSegments.isEmpty()) && fromScratch) {
 287                         throw new IIOInvalidTreeException
 288                             ("app0JFIF node cannot have > 1 JFXX node", node);
 289                     }
 290                     NodeList exts = child.getChildNodes();
 291                     int extCount = exts.getLength();
 292                     for (int j = 0; j < extCount; j++) {
 293                         Node ext = exts.item(j);
 294                         extSegments.add(new JFIFExtensionMarkerSegment(ext));
 295                     }
 296                 }
 297                 if (name.equals("app2ICC")) {
 298                     if ((iccSegment != null) && fromScratch) {
 299                         throw new IIOInvalidTreeException
 300                             ("> 1 ICC APP2 Marker Segment not supported", node);
 301                     }
 302                     iccSegment = new ICCMarkerSegment(child);
 303                 }
 304             }
 305         }
 306     }
 307 
 308     int getThumbnailWidth(int index) {
 309         if (thumb != null) {
 310             if (index == 0) {
 311                 return thumb.getWidth();
 312             }
 313             index--;
 314         }
 315         JFIFExtensionMarkerSegment jfxx =
 316             (JFIFExtensionMarkerSegment) extSegments.get(index);
 317         return jfxx.thumb.getWidth();
 318     }
 319 
 320     int getThumbnailHeight(int index) {
 321         if (thumb != null) {
 322             if (index == 0) {
 323                 return thumb.getHeight();
 324             }
 325             index--;
 326         }
 327         JFIFExtensionMarkerSegment jfxx =
 328             (JFIFExtensionMarkerSegment) extSegments.get(index);
 329         return jfxx.thumb.getHeight();
 330     }
 331 
 332     BufferedImage getThumbnail(ImageInputStream iis,
 333                                int index,
 334                                JPEGImageReader reader) throws IOException {
 335         reader.thumbnailStarted(index);
 336         BufferedImage ret = null;
 337         if ((thumb != null) && (index == 0)) {
 338                 ret = thumb.getThumbnail(iis, reader);
 339         } else {
 340             if (thumb != null) {
 341                 index--;
 342             }
 343             JFIFExtensionMarkerSegment jfxx =
 344                 (JFIFExtensionMarkerSegment) extSegments.get(index);
 345             ret = jfxx.thumb.getThumbnail(iis, reader);
 346         }
 347         reader.thumbnailComplete();
 348         return ret;
 349     }
 350 
 351 
 352     /**
 353      * Writes the data for this segment to the stream in
 354      * valid JPEG format.  Assumes that there will be no thumbnail.
 355      */
 356     void write(ImageOutputStream ios,
 357                JPEGImageWriter writer) throws IOException {
 358         // No thumbnail
 359         write(ios, null, writer);
 360     }
 361 
 362     /**
 363      * Writes the data for this segment to the stream in
 364      * valid JPEG format.  The length written takes the thumbnail
 365      * width and height into account.  If necessary, the thumbnail
 366      * is clipped to 255 x 255 and a warning is sent to the writer
 367      * argument.  Progress updates are sent to the writer argument.
 368      */
 369     void write(ImageOutputStream ios,
 370                BufferedImage thumb,
 371                JPEGImageWriter writer) throws IOException {
 372         int thumbWidth = 0;
 373         int thumbHeight = 0;
 374         int thumbLength = 0;
 375         int [] thumbData = null;
 376         if (thumb != null) {
 377             // Clip if necessary and get the data in thumbData
 378             thumbWidth = thumb.getWidth();
 379             thumbHeight = thumb.getHeight();
 380             if ((thumbWidth > MAX_THUMB_WIDTH)
 381                 || (thumbHeight > MAX_THUMB_HEIGHT)) {
 382                 writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
 383             }
 384             thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
 385             thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
 386             thumbData = thumb.getRaster().getPixels(0, 0,
 387                                                     thumbWidth, thumbHeight,
 388                                                     (int []) null);
 389             thumbLength = thumbData.length;
 390         }
 391         length = DATA_SIZE + LENGTH_SIZE + thumbLength;
 392         writeTag(ios);
 393         byte [] id = {0x4A, 0x46, 0x49, 0x46, 0x00};
 394         ios.write(id);
 395         ios.write(majorVersion);
 396         ios.write(minorVersion);
 397         ios.write(resUnits);
 398         write2bytes(ios, Xdensity);
 399         write2bytes(ios, Ydensity);
 400         ios.write(thumbWidth);
 401         ios.write(thumbHeight);
 402         if (thumbData != null) {
 403             writer.thumbnailStarted(0);
 404             writeThumbnailData(ios, thumbData, writer);
 405             writer.thumbnailComplete();
 406         }
 407     }
 408 
 409     /*
 410      * Write out the values in the integer array as a sequence of bytes,
 411      * reporting progress to the writer argument.
 412      */
 413     void writeThumbnailData(ImageOutputStream ios,
 414                             int [] thumbData,
 415                             JPEGImageWriter writer) throws IOException {
 416         int progInterval = thumbData.length / 20;  // approx. every 5%
 417         if (progInterval == 0) {
 418             progInterval = 1;
 419         }
 420         for (int i = 0; i < thumbData.length; i++) {
 421             ios.write(thumbData[i]);
 422             if ((i > progInterval) && (i % progInterval == 0)) {
 423                 writer.thumbnailProgress
 424                     (((float) i * 100) / ((float) thumbData.length));
 425             }
 426         }
 427     }
 428 
 429     /**
 430      * Write out this JFIF Marker Segment, including a thumbnail or
 431      * appending a series of JFXX Marker Segments, as appropriate.
 432      * Warnings and progress reports are sent to the writer argument.
 433      * The list of thumbnails is matched to the list of JFXX extension
 434      * segments, if any, in order to determine how to encode the
 435      * thumbnails.  If there are more thumbnails than metadata segments,
 436      * default encoding is used for the extra thumbnails.
 437      */
 438     void writeWithThumbs(ImageOutputStream ios,
 439                          List thumbnails,
 440                          JPEGImageWriter writer) throws IOException {
 441         if (thumbnails != null) {
 442             JFIFExtensionMarkerSegment jfxx = null;
 443             if (thumbnails.size() == 1) {
 444                 if (!extSegments.isEmpty()) {
 445                     jfxx = (JFIFExtensionMarkerSegment) extSegments.get(0);
 446                 }
 447                 writeThumb(ios,
 448                            (BufferedImage) thumbnails.get(0),
 449                            jfxx,
 450                            0,
 451                            true,
 452                            writer);
 453             } else {
 454                 // All others write as separate JFXX segments
 455                 write(ios, writer);  // Just the header without any thumbnail
 456                 for (int i = 0; i < thumbnails.size(); i++) {
 457                     jfxx = null;
 458                     if (i < extSegments.size()) {
 459                         jfxx = (JFIFExtensionMarkerSegment) extSegments.get(i);
 460                     }
 461                     writeThumb(ios,
 462                                (BufferedImage) thumbnails.get(i),
 463                                jfxx,
 464                                i,
 465                                false,
 466                                writer);
 467                 }
 468             }
 469         } else {  // No thumbnails
 470             write(ios, writer);
 471         }
 472 
 473     }
 474 
 475     private void writeThumb(ImageOutputStream ios,
 476                             BufferedImage thumb,
 477                             JFIFExtensionMarkerSegment jfxx,
 478                             int index,
 479                             boolean onlyOne,
 480                             JPEGImageWriter writer) throws IOException {
 481         ColorModel cm = thumb.getColorModel();
 482         ColorSpace cs = cm.getColorSpace();
 483 
 484         if (cm instanceof IndexColorModel) {
 485             // We never write a palette image into the header
 486             // So if it's the only one, we need to write the header first
 487             if (onlyOne) {
 488                 write(ios, writer);
 489             }
 490             if ((jfxx == null)
 491                 || (jfxx.code == THUMB_PALETTE)) {
 492                 writeJFXXSegment(index, thumb, ios, writer); // default
 493             } else {
 494                 // Expand to RGB
 495                 BufferedImage thumbRGB =
 496                     ((IndexColorModel) cm).convertToIntDiscrete
 497                     (thumb.getRaster(), false);
 498                 jfxx.setThumbnail(thumbRGB);
 499                 writer.thumbnailStarted(index);
 500                 jfxx.write(ios, writer);  // Handles clipping if needed
 501                 writer.thumbnailComplete();
 502             }
 503         } else if (cs.getType() == ColorSpace.TYPE_RGB) {
 504             if (jfxx == null) {
 505                 if (onlyOne) {
 506                     write(ios, thumb, writer); // As part of the header
 507                 } else {
 508                     writeJFXXSegment(index, thumb, ios, writer); // default
 509                 }
 510             } else {
 511                 // If this is the only one, write the header first
 512                 if (onlyOne) {
 513                     write(ios, writer);
 514                 }
 515                 if (jfxx.code == THUMB_PALETTE) {
 516                     writeJFXXSegment(index, thumb, ios, writer); // default
 517                     writer.warningOccurred
 518                         (JPEGImageWriter.WARNING_NO_RGB_THUMB_AS_INDEXED);
 519                 } else {
 520                     jfxx.setThumbnail(thumb);
 521                     writer.thumbnailStarted(index);
 522                     jfxx.write(ios, writer);  // Handles clipping if needed
 523                     writer.thumbnailComplete();
 524                 }
 525             }
 526         } else if (cs.getType() == ColorSpace.TYPE_GRAY) {
 527             if (jfxx == null) {
 528                 if (onlyOne) {
 529                     BufferedImage thumbRGB = expandGrayThumb(thumb);
 530                     write(ios, thumbRGB, writer); // As part of the header
 531                 } else {
 532                     writeJFXXSegment(index, thumb, ios, writer); // default
 533                 }
 534             } else {
 535                 // If this is the only one, write the header first
 536                 if (onlyOne) {
 537                     write(ios, writer);
 538                 }
 539                 if (jfxx.code == THUMB_RGB) {
 540                     BufferedImage thumbRGB = expandGrayThumb(thumb);
 541                     writeJFXXSegment(index, thumbRGB, ios, writer);
 542                 } else if (jfxx.code == THUMB_JPEG) {
 543                     jfxx.setThumbnail(thumb);
 544                     writer.thumbnailStarted(index);
 545                     jfxx.write(ios, writer);  // Handles clipping if needed
 546                     writer.thumbnailComplete();
 547                 } else if (jfxx.code == THUMB_PALETTE) {
 548                     writeJFXXSegment(index, thumb, ios, writer); // default
 549                     writer.warningOccurred
 550                         (JPEGImageWriter.WARNING_NO_GRAY_THUMB_AS_INDEXED);
 551                 }
 552             }
 553         } else {
 554             writer.warningOccurred
 555                 (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
 556         }
 557     }
 558 
 559     // Could put reason codes in here to be parsed in writeJFXXSegment
 560     // in order to provide more meaningful warnings.
 561     @SuppressWarnings("serial") // JDK-implementation class
 562     private class IllegalThumbException extends Exception {}
 563 
 564     /**
 565      * Writes out a new JFXX extension segment, without saving it.
 566      */
 567     private void writeJFXXSegment(int index,
 568                                   BufferedImage thumbnail,
 569                                   ImageOutputStream ios,
 570                                   JPEGImageWriter writer) throws IOException {
 571         JFIFExtensionMarkerSegment jfxx = null;
 572         try {
 573              jfxx = new JFIFExtensionMarkerSegment(thumbnail);
 574         } catch (IllegalThumbException e) {
 575             writer.warningOccurred
 576                 (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
 577             return;
 578         }
 579         writer.thumbnailStarted(index);
 580         jfxx.write(ios, writer);
 581         writer.thumbnailComplete();
 582     }
 583 
 584 
 585     /**
 586      * Return an RGB image that is the expansion of the given grayscale
 587      * image.
 588      */
 589     private static BufferedImage expandGrayThumb(BufferedImage thumb) {
 590         BufferedImage ret = new BufferedImage(thumb.getWidth(),
 591                                               thumb.getHeight(),
 592                                               BufferedImage.TYPE_INT_RGB);
 593         Graphics g = ret.getGraphics();
 594         g.drawImage(thumb, 0, 0, null);
 595         return ret;
 596     }
 597 
 598     /**
 599      * Writes out a default JFIF marker segment to the given
 600      * output stream.  If <code>thumbnails</code> is not <code>null</code>,
 601      * writes out the set of thumbnail images as JFXX marker segments, or
 602      * incorporated into the JFIF segment if appropriate.
 603      * If <code>iccProfile</code> is not <code>null</code>,
 604      * writes out the profile after the JFIF segment using as many APP2
 605      * marker segments as necessary.
 606      */
 607     static void writeDefaultJFIF(ImageOutputStream ios,
 608                                  List thumbnails,
 609                                  ICC_Profile iccProfile,
 610                                  JPEGImageWriter writer)
 611         throws IOException {
 612 
 613         JFIFMarkerSegment jfif = new JFIFMarkerSegment();
 614         jfif.writeWithThumbs(ios, thumbnails, writer);
 615         if (iccProfile != null) {
 616             writeICC(iccProfile, ios);
 617         }
 618     }
 619 
 620     /**
 621      * Prints out the contents of this object to System.out for debugging.
 622      */
 623     void print() {
 624         printTag("JFIF");
 625         System.out.print("Version ");
 626         System.out.print(majorVersion);
 627         System.out.println(".0"
 628                            + Integer.toString(minorVersion));
 629         System.out.print("Resolution units: ");
 630         System.out.println(resUnits);
 631         System.out.print("X density: ");
 632         System.out.println(Xdensity);
 633         System.out.print("Y density: ");
 634         System.out.println(Ydensity);
 635         System.out.print("Thumbnail Width: ");
 636         System.out.println(thumbWidth);
 637         System.out.print("Thumbnail Height: ");
 638         System.out.println(thumbHeight);
 639         if (!extSegments.isEmpty()) {
 640             for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
 641                 JFIFExtensionMarkerSegment extSegment =
 642                     (JFIFExtensionMarkerSegment) iter.next();
 643                 extSegment.print();
 644             }
 645         }
 646         if (iccSegment != null) {
 647             iccSegment.print();
 648         }
 649     }
 650 
 651     /**
 652      * A JFIF extension APP0 marker segment.
 653      */
 654     class JFIFExtensionMarkerSegment extends MarkerSegment {
 655         int code;
 656         JFIFThumb thumb;
 657         private static final int DATA_SIZE = 6;
 658         private static final int ID_SIZE = 5;
 659 
 660         JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)
 661             throws IOException {
 662 
 663             super(buffer);
 664             buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it
 665 
 666             code = buffer.buf[buffer.bufPtr++] & 0xff;
 667             buffer.bufAvail -= DATA_SIZE;
 668             if (code == THUMB_JPEG) {
 669                 thumb = new JFIFThumbJPEG(buffer, length, reader);
 670             } else {
 671                 buffer.loadBuf(2);
 672                 int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
 673                 int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
 674                 buffer.bufAvail -= 2;
 675                 // following constructors handle bufAvail
 676                 if (code == THUMB_PALETTE) {
 677                     thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
 678                 } else {
 679                     thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
 680                 }
 681             }
 682         }
 683 
 684         JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
 685             super(JPEG.APP0);
 686             NamedNodeMap attrs = node.getAttributes();
 687             if (attrs.getLength() > 0) {
 688                 code = getAttributeValue(node,
 689                                          attrs,
 690                                          "extensionCode",
 691                                          THUMB_JPEG,
 692                                          THUMB_RGB,
 693                                          false);
 694                 if (code == THUMB_UNASSIGNED) {
 695                 throw new IIOInvalidTreeException
 696                     ("invalid extensionCode attribute value", node);
 697                 }
 698             } else {
 699                 code = THUMB_UNASSIGNED;
 700             }
 701             // Now the child
 702             if (node.getChildNodes().getLength() != 1) {
 703                 throw new IIOInvalidTreeException
 704                     ("app0JFXX node must have exactly 1 child", node);
 705             }
 706             Node child = node.getFirstChild();
 707             String name = child.getNodeName();
 708             if (name.equals("JFIFthumbJPEG")) {
 709                 if (code == THUMB_UNASSIGNED) {
 710                     code = THUMB_JPEG;
 711                 }
 712                 thumb = new JFIFThumbJPEG(child);
 713             } else if (name.equals("JFIFthumbPalette")) {
 714                 if (code == THUMB_UNASSIGNED) {
 715                     code = THUMB_PALETTE;
 716                 }
 717                 thumb = new JFIFThumbPalette(child);
 718             } else if (name.equals("JFIFthumbRGB")) {
 719                 if (code == THUMB_UNASSIGNED) {
 720                     code = THUMB_RGB;
 721                 }
 722                 thumb = new JFIFThumbRGB(child);
 723             } else {
 724                 throw new IIOInvalidTreeException
 725                     ("unrecognized app0JFXX child node", node);
 726             }
 727         }
 728 
 729         JFIFExtensionMarkerSegment(BufferedImage thumbnail)
 730             throws IllegalThumbException {
 731 
 732             super(JPEG.APP0);
 733             ColorModel cm = thumbnail.getColorModel();
 734             int csType = cm.getColorSpace().getType();
 735             if (cm.hasAlpha()) {
 736                 throw new IllegalThumbException();
 737             }
 738             if (cm instanceof IndexColorModel) {
 739                 code = THUMB_PALETTE;
 740                 thumb = new JFIFThumbPalette(thumbnail);
 741             } else if (csType == ColorSpace.TYPE_RGB) {
 742                 code = THUMB_RGB;
 743                 thumb = new JFIFThumbRGB(thumbnail);
 744             } else if (csType == ColorSpace.TYPE_GRAY) {
 745                 code = THUMB_JPEG;
 746                 thumb = new JFIFThumbJPEG(thumbnail);
 747             } else {
 748                 throw new IllegalThumbException();
 749             }
 750         }
 751 
 752         void setThumbnail(BufferedImage thumbnail) {
 753             try {
 754                 switch (code) {
 755                 case THUMB_PALETTE:
 756                     thumb = new JFIFThumbPalette(thumbnail);
 757                     break;
 758                 case THUMB_RGB:
 759                     thumb = new JFIFThumbRGB(thumbnail);
 760                     break;
 761                 case THUMB_JPEG:
 762                     thumb = new JFIFThumbJPEG(thumbnail);
 763                     break;
 764                 }
 765             } catch (IllegalThumbException e) {
 766                 // Should never happen
 767                 throw new InternalError("Illegal thumb in setThumbnail!", e);
 768             }
 769         }
 770 
 771         protected Object clone() {
 772             JFIFExtensionMarkerSegment newGuy =
 773                 (JFIFExtensionMarkerSegment) super.clone();
 774             if (thumb != null) {
 775                 newGuy.thumb = (JFIFThumb) thumb.clone();
 776             }
 777             return newGuy;
 778         }
 779 
 780         IIOMetadataNode getNativeNode() {
 781             IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
 782             node.setAttribute("extensionCode", Integer.toString(code));
 783             node.appendChild(thumb.getNativeNode());
 784             return node;
 785         }
 786 
 787         void write(ImageOutputStream ios,
 788                    JPEGImageWriter writer) throws IOException {
 789             length = LENGTH_SIZE + DATA_SIZE + thumb.getLength();
 790             writeTag(ios);
 791             byte [] id = {0x4A, 0x46, 0x58, 0x58, 0x00};
 792             ios.write(id);
 793             ios.write(code);
 794             thumb.write(ios, writer);
 795         }
 796 
 797         void print() {
 798             printTag("JFXX");
 799             thumb.print();
 800         }
 801     }
 802 
 803     /**
 804      * A superclass for the varieties of thumbnails that can
 805      * be stored in a JFIF extension marker segment.
 806      */
 807     abstract class JFIFThumb implements Cloneable {
 808         long streamPos = -1L;  // Save the thumbnail pos when reading
 809         abstract int getLength(); // When writing
 810         abstract int getWidth();
 811         abstract int getHeight();
 812         abstract BufferedImage getThumbnail(ImageInputStream iis,
 813                                             JPEGImageReader reader)
 814             throws IOException;
 815 
 816         protected JFIFThumb() {}
 817 
 818         protected JFIFThumb(JPEGBuffer buffer) throws IOException{
 819             // Save the stream position for reading the thumbnail later
 820             streamPos = buffer.getStreamPosition();
 821         }
 822 
 823         abstract void print();
 824 
 825         abstract IIOMetadataNode getNativeNode();
 826 
 827         abstract void write(ImageOutputStream ios,
 828                             JPEGImageWriter writer) throws IOException;
 829 
 830         protected Object clone() {
 831             try {
 832                 return super.clone();
 833             } catch (CloneNotSupportedException e) {} // won't happen
 834             return null;
 835         }
 836 
 837     }
 838 
 839     abstract class JFIFThumbUncompressed extends JFIFThumb {
 840         BufferedImage thumbnail = null;
 841         int thumbWidth;
 842         int thumbHeight;
 843         String name;
 844 
 845         JFIFThumbUncompressed(JPEGBuffer buffer,
 846                               int width,
 847                               int height,
 848                               int skip,
 849                               String name)
 850             throws IOException {
 851             super(buffer);
 852             thumbWidth = width;
 853             thumbHeight = height;
 854             // Now skip the thumbnail data
 855             buffer.skipData(skip);
 856             this.name = name;
 857         }
 858 
 859         JFIFThumbUncompressed(Node node, String name)
 860             throws IIOInvalidTreeException {
 861 
 862             thumbWidth = 0;
 863             thumbHeight = 0;
 864             this.name = name;
 865             NamedNodeMap attrs = node.getAttributes();
 866             int count = attrs.getLength();
 867             if (count > 2) {
 868                 throw new IIOInvalidTreeException
 869                     (name +" node cannot have > 2 attributes", node);
 870             }
 871             if (count != 0) {
 872                 int value = getAttributeValue(node, attrs, "thumbWidth",
 873                                               0, 255, false);
 874                 thumbWidth = (value != -1) ? value : thumbWidth;
 875                 value = getAttributeValue(node, attrs, "thumbHeight",
 876                                           0, 255, false);
 877                 thumbHeight = (value != -1) ? value : thumbHeight;
 878             }
 879         }
 880 
 881         JFIFThumbUncompressed(BufferedImage thumb) {
 882             thumbnail = thumb;
 883             thumbWidth = thumb.getWidth();
 884             thumbHeight = thumb.getHeight();
 885             name = null;  // not used when writing
 886         }
 887 
 888         void readByteBuffer(ImageInputStream iis,
 889                             byte [] data,
 890                             JPEGImageReader reader,
 891                             float workPortion,
 892                             float workOffset) throws IOException {
 893             int progInterval = Math.max((int)(data.length/20/workPortion),
 894                                         1);
 895             for (int offset = 0;
 896                  offset < data.length;) {
 897                 int len = Math.min(progInterval, data.length-offset);
 898                 iis.read(data, offset, len);
 899                 offset += progInterval;
 900                 float percentDone = ((float) offset* 100)
 901                     / data.length
 902                     * workPortion + workOffset;
 903                 if (percentDone > 100.0F) {
 904                     percentDone = 100.0F;
 905                 }
 906                 reader.thumbnailProgress (percentDone);
 907             }
 908         }
 909 
 910 
 911         int getWidth() {
 912             return thumbWidth;
 913         }
 914 
 915         int getHeight() {
 916             return thumbHeight;
 917         }
 918 
 919         IIOMetadataNode getNativeNode() {
 920             IIOMetadataNode node = new IIOMetadataNode(name);
 921             node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
 922             node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
 923             return node;
 924         }
 925 
 926         void write(ImageOutputStream ios,
 927                    JPEGImageWriter writer) throws IOException {
 928             if ((thumbWidth > MAX_THUMB_WIDTH)
 929                 || (thumbHeight > MAX_THUMB_HEIGHT)) {
 930                 writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
 931             }
 932             thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
 933             thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
 934             ios.write(thumbWidth);
 935             ios.write(thumbHeight);
 936         }
 937 
 938         void writePixels(ImageOutputStream ios,
 939                          JPEGImageWriter writer) throws IOException {
 940             if ((thumbWidth > MAX_THUMB_WIDTH)
 941                 || (thumbHeight > MAX_THUMB_HEIGHT)) {
 942                 writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
 943             }
 944             thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
 945             thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
 946             int [] data = thumbnail.getRaster().getPixels(0, 0,
 947                                                           thumbWidth,
 948                                                           thumbHeight,
 949                                                           (int []) null);
 950             writeThumbnailData(ios, data, writer);
 951         }
 952 
 953         void print() {
 954             System.out.print(name + " width: ");
 955             System.out.println(thumbWidth);
 956             System.out.print(name + " height: ");
 957             System.out.println(thumbHeight);
 958         }
 959 
 960     }
 961 
 962     /**
 963      * A JFIF thumbnail stored as RGB, one byte per channel,
 964      * interleaved.
 965      */
 966     class JFIFThumbRGB extends JFIFThumbUncompressed {
 967 
 968         JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
 969             throws IOException {
 970 
 971             super(buffer, width, height, width*height*3, "JFIFthumbRGB");
 972         }
 973 
 974         JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
 975             super(node, "JFIFthumbRGB");
 976         }
 977 
 978         JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
 979             super(thumb);
 980         }
 981 
 982         int getLength() {
 983             return (thumbWidth*thumbHeight*3);
 984         }
 985 
 986         BufferedImage getThumbnail(ImageInputStream iis,
 987                                    JPEGImageReader reader)
 988             throws IOException {
 989             iis.mark();
 990             iis.seek(streamPos);
 991             DataBufferByte buffer = new DataBufferByte(getLength());
 992             readByteBuffer(iis,
 993                            buffer.getData(),
 994                            reader,
 995                            1.0F,
 996                            0.0F);
 997             iis.reset();
 998 
 999             WritableRaster raster =
1000                 Raster.createInterleavedRaster(buffer,
1001                                                thumbWidth,
1002                                                thumbHeight,
1003                                                thumbWidth*3,
1004                                                3,
1005                                                new int [] {0, 1, 2},
1006                                                null);
1007             ColorModel cm = new ComponentColorModel(JPEG.JCS.sRGB,
1008                                                     false,
1009                                                     false,
1010                                                     ColorModel.OPAQUE,
1011                                                     DataBuffer.TYPE_BYTE);
1012             return new BufferedImage(cm,
1013                                      raster,
1014                                      false,
1015                                      null);
1016         }
1017 
1018         void write(ImageOutputStream ios,
1019                    JPEGImageWriter writer) throws IOException {
1020             super.write(ios, writer); // width and height
1021             writePixels(ios, writer);
1022         }
1023 
1024     }
1025 
1026     /**
1027      * A JFIF thumbnail stored as an indexed palette image
1028      * using an RGB palette.
1029      */
1030     class JFIFThumbPalette extends JFIFThumbUncompressed {
1031         private static final int PALETTE_SIZE = 768;
1032 
1033         JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
1034             throws IOException {
1035             super(buffer,
1036                   width,
1037                   height,
1038                   PALETTE_SIZE + width * height,
1039                   "JFIFThumbPalette");
1040         }
1041 
1042         JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
1043             super(node, "JFIFThumbPalette");
1044         }
1045 
1046         JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException {
1047             super(thumb);
1048             IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
1049             if (icm.getMapSize() > 256) {
1050                 throw new IllegalThumbException();
1051             }
1052         }
1053 
1054         int getLength() {
1055             return (thumbWidth*thumbHeight + PALETTE_SIZE);
1056         }
1057 
1058         BufferedImage getThumbnail(ImageInputStream iis,
1059                                    JPEGImageReader reader)
1060             throws IOException {
1061             iis.mark();
1062             iis.seek(streamPos);
1063             // read the palette
1064             byte [] palette = new byte [PALETTE_SIZE];
1065             float palettePart = ((float) PALETTE_SIZE) / getLength();
1066             readByteBuffer(iis,
1067                            palette,
1068                            reader,
1069                            palettePart,
1070                            0.0F);
1071             DataBufferByte buffer = new DataBufferByte(thumbWidth*thumbHeight);
1072             readByteBuffer(iis,
1073                            buffer.getData(),
1074                            reader,
1075                            1.0F-palettePart,
1076                            palettePart);
1077             iis.read();
1078             iis.reset();
1079 
1080             IndexColorModel cm = new IndexColorModel(8,
1081                                                      256,
1082                                                      palette,
1083                                                      0,
1084                                                      false);
1085             SampleModel sm = cm.createCompatibleSampleModel(thumbWidth,
1086                                                             thumbHeight);
1087             WritableRaster raster =
1088                 Raster.createWritableRaster(sm, buffer, null);
1089             return new BufferedImage(cm,
1090                                      raster,
1091                                      false,
1092                                      null);
1093         }
1094 
1095         void write(ImageOutputStream ios,
1096                    JPEGImageWriter writer) throws IOException {
1097             super.write(ios, writer); // width and height
1098             // Write the palette (must be 768 bytes)
1099             byte [] palette = new byte[768];
1100             IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
1101             byte [] reds = new byte [256];
1102             byte [] greens = new byte [256];
1103             byte [] blues = new byte [256];
1104             icm.getReds(reds);
1105             icm.getGreens(greens);
1106             icm.getBlues(blues);
1107             for (int i = 0; i < 256; i++) {
1108                 palette[i*3] = reds[i];
1109                 palette[i*3+1] = greens[i];
1110                 palette[i*3+2] = blues[i];
1111             }
1112             ios.write(palette);
1113             writePixels(ios, writer);
1114         }
1115     }
1116 
1117 
1118     /**
1119      * A JFIF thumbnail stored as a JPEG stream.  No JFIF or
1120      * JFIF extension markers are permitted.  There is no need
1121      * to clip these, but the entire image must fit into a
1122      * single JFXX marker segment.
1123      */
1124     class JFIFThumbJPEG extends JFIFThumb {
1125         JPEGMetadata thumbMetadata = null;
1126         byte [] data = null;  // Compressed image data, for writing
1127         private static final int PREAMBLE_SIZE = 6;
1128 
1129         JFIFThumbJPEG(JPEGBuffer buffer,
1130                       int length,
1131                       JPEGImageReader reader) throws IOException {
1132             super(buffer);
1133             // Compute the final stream position
1134             long finalPos = streamPos + (length - PREAMBLE_SIZE);
1135             // Set the stream back to the start of the thumbnail
1136             // and read its metadata (but don't decode the image)
1137             buffer.iis.seek(streamPos);
1138             thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
1139             // Set the stream to the computed final position
1140             buffer.iis.seek(finalPos);
1141             // Clear the now invalid buffer
1142             buffer.bufAvail = 0;
1143             buffer.bufPtr = 0;
1144         }
1145 
1146         JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
1147             if (node.getChildNodes().getLength() > 1) {
1148                 throw new IIOInvalidTreeException
1149                     ("JFIFThumbJPEG node must have 0 or 1 child", node);
1150             }
1151             Node child = node.getFirstChild();
1152             if (child != null) {
1153                 String name = child.getNodeName();
1154                 if (!name.equals("markerSequence")) {
1155                     throw new IIOInvalidTreeException
1156                         ("JFIFThumbJPEG child must be a markerSequence node",
1157                          node);
1158                 }
1159                 thumbMetadata = new JPEGMetadata(false, true);
1160                 thumbMetadata.setFromMarkerSequenceNode(child);
1161             }
1162         }
1163 
1164         JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException {
1165             int INITIAL_BUFSIZE = 4096;
1166             int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE;
1167             try {
1168                 ByteArrayOutputStream baos =
1169                     new ByteArrayOutputStream(INITIAL_BUFSIZE);
1170                 MemoryCacheImageOutputStream mos =
1171                     new MemoryCacheImageOutputStream(baos);
1172 
1173                 JPEGImageWriter thumbWriter = new JPEGImageWriter(null);
1174 
1175                 thumbWriter.setOutput(mos);
1176 
1177                 // get default metadata for the thumb
1178                 JPEGMetadata metadata =
1179                     (JPEGMetadata) thumbWriter.getDefaultImageMetadata
1180                     (new ImageTypeSpecifier(thumb), null);
1181 
1182                 // Remove the jfif segment, which should be there.
1183                 MarkerSegment jfif = metadata.findMarkerSegment
1184                     (JFIFMarkerSegment.class, true);
1185                 if (jfif == null) {
1186                     throw new IllegalThumbException();
1187                 }
1188 
1189                 metadata.markerSequence.remove(jfif);
1190 
1191                 /*  Use this if removing leaves a hole and causes trouble
1192 
1193                 // Get the tree
1194                 String format = metadata.getNativeMetadataFormatName();
1195                 IIOMetadataNode tree =
1196                 (IIOMetadataNode) metadata.getAsTree(format);
1197 
1198                 // If there is no app0jfif node, the image is bad
1199                 NodeList jfifs = tree.getElementsByTagName("app0JFIF");
1200                 if (jfifs.getLength() == 0) {
1201                 throw new IllegalThumbException();
1202                 }
1203 
1204                 // remove the app0jfif node
1205                 Node jfif = jfifs.item(0);
1206                 Node parent = jfif.getParentNode();
1207                 parent.removeChild(jfif);
1208 
1209                 metadata.setFromTree(format, tree);
1210                 */
1211 
1212                 thumbWriter.write(new IIOImage(thumb, null, metadata));
1213 
1214                 thumbWriter.dispose();
1215                 // Now check that the size is OK
1216                 if (baos.size() > MAZ_BUFSIZE) {
1217                     throw new IllegalThumbException();
1218                 }
1219                 data = baos.toByteArray();
1220             } catch (IOException e) {
1221                 throw new IllegalThumbException();
1222             }
1223         }
1224 
1225         int getWidth() {
1226             int retval = 0;
1227             SOFMarkerSegment sof =
1228                 (SOFMarkerSegment) thumbMetadata.findMarkerSegment
1229                 (SOFMarkerSegment.class, true);
1230             if (sof != null) {
1231                 retval = sof.samplesPerLine;
1232             }
1233             return retval;
1234         }
1235 
1236         int getHeight() {
1237             int retval = 0;
1238             SOFMarkerSegment sof =
1239                 (SOFMarkerSegment) thumbMetadata.findMarkerSegment
1240                 (SOFMarkerSegment.class, true);
1241             if (sof != null) {
1242                 retval = sof.numLines;
1243             }
1244             return retval;
1245         }
1246 
1247         private class ThumbnailReadListener
1248             implements IIOReadProgressListener {
1249             JPEGImageReader reader = null;
1250             ThumbnailReadListener (JPEGImageReader reader) {
1251                 this.reader = reader;
1252             }
1253             public void sequenceStarted(ImageReader source, int minIndex) {}
1254             public void sequenceComplete(ImageReader source) {}
1255             public void imageStarted(ImageReader source, int imageIndex) {}
1256             public void imageProgress(ImageReader source,
1257                                       float percentageDone) {
1258                 reader.thumbnailProgress(percentageDone);
1259             }
1260             public void imageComplete(ImageReader source) {}
1261             public void thumbnailStarted(ImageReader source,
1262                 int imageIndex, int thumbnailIndex) {}
1263             public void thumbnailProgress(ImageReader source, float percentageDone) {}
1264             public void thumbnailComplete(ImageReader source) {}
1265             public void readAborted(ImageReader source) {}
1266         }
1267 
1268         BufferedImage getThumbnail(ImageInputStream iis,
1269                                    JPEGImageReader reader)
1270             throws IOException {
1271             iis.mark();
1272             iis.seek(streamPos);
1273             JPEGImageReader thumbReader = new JPEGImageReader(null);
1274             thumbReader.setInput(iis);
1275             thumbReader.addIIOReadProgressListener
1276                 (new ThumbnailReadListener(reader));
1277             BufferedImage ret = thumbReader.read(0, null);
1278             thumbReader.dispose();
1279             iis.reset();
1280             return ret;
1281         }
1282 
1283         protected Object clone() {
1284             JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
1285             if (thumbMetadata != null) {
1286                 newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
1287             }
1288             return newGuy;
1289         }
1290 
1291         IIOMetadataNode getNativeNode() {
1292             IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
1293             if (thumbMetadata != null) {
1294                 node.appendChild(thumbMetadata.getNativeTree());
1295             }
1296             return node;
1297         }
1298 
1299         int getLength() {
1300             if (data == null) {
1301                 return 0;
1302             } else {
1303                 return data.length;
1304             }
1305         }
1306 
1307         void write(ImageOutputStream ios,
1308                    JPEGImageWriter writer) throws IOException {
1309             int progInterval = data.length / 20;  // approx. every 5%
1310             if (progInterval == 0) {
1311                 progInterval = 1;
1312             }
1313             for (int offset = 0;
1314                  offset < data.length;) {
1315                 int len = Math.min(progInterval, data.length-offset);
1316                 ios.write(data, offset, len);
1317                 offset += progInterval;
1318                 float percentDone = ((float) offset * 100) / data.length;
1319                 if (percentDone > 100.0F) {
1320                     percentDone = 100.0F;
1321                 }
1322                 writer.thumbnailProgress (percentDone);
1323             }
1324         }
1325 
1326         void print () {
1327             System.out.println("JFIF thumbnail stored as JPEG");
1328         }
1329     }
1330 
1331     /**
1332      * Write out the given profile to the stream, embedded in
1333      * the necessary number of APP2 segments, per the ICC spec.
1334      * This is the only mechanism for writing an ICC profile
1335      * to a stream.
1336      */
1337     static void writeICC(ICC_Profile profile, ImageOutputStream ios)
1338         throws IOException {
1339         int LENGTH_LENGTH = 2;
1340         final String ID = "ICC_PROFILE";
1341         int ID_LENGTH = ID.length()+1; // spec says it's null-terminated
1342         int COUNTS_LENGTH = 2;
1343         int MAX_ICC_CHUNK_SIZE =
1344             65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH;
1345 
1346         byte [] data = profile.getData();
1347         int numChunks = data.length / MAX_ICC_CHUNK_SIZE;
1348         if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) {
1349             numChunks++;
1350         }
1351         int chunkNum = 1;
1352         int offset = 0;
1353         for (int i = 0; i < numChunks; i++) {
1354             int dataLength = Math.min(data.length-offset, MAX_ICC_CHUNK_SIZE);
1355             int segLength = dataLength+COUNTS_LENGTH+ID_LENGTH+LENGTH_LENGTH;
1356             ios.write(0xff);
1357             ios.write(JPEG.APP2);
1358             MarkerSegment.write2bytes(ios, segLength);
1359             byte [] id = ID.getBytes("US-ASCII");
1360             ios.write(id);
1361             ios.write(0); // Null-terminate the string
1362             ios.write(chunkNum++);
1363             ios.write(numChunks);
1364             ios.write(data, offset, dataLength);
1365             offset += dataLength;
1366         }
1367     }
1368 
1369     /**
1370      * An APP2 marker segment containing an ICC profile.  In the stream
1371      * a profile larger than 64K is broken up into a series of chunks.
1372      * This inner class represents the complete profile as a single object,
1373      * combining chunks as necessary.
1374      */
1375     class ICCMarkerSegment extends MarkerSegment {
1376         ArrayList chunks = null;
1377         byte [] profile = null; // The complete profile when it's fully read
1378                          // May remain null when writing
1379         private static final int ID_SIZE = 12;
1380         int chunksRead;
1381         int numChunks;
1382 
1383         ICCMarkerSegment(ICC_ColorSpace cs) {
1384             super(JPEG.APP2);
1385             chunks = null;
1386             chunksRead = 0;
1387             numChunks = 0;
1388             profile = cs.getProfile().getData();
1389         }
1390 
1391         ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
1392             super(buffer);  // gets whole segment or fills the buffer
1393             if (debug) {
1394                 System.out.println("Creating new ICC segment");
1395             }
1396             buffer.bufPtr += ID_SIZE; // Skip the id
1397             buffer.bufAvail -= ID_SIZE;
1398             /*
1399              * Reduce the stored length by the id size.  The stored
1400              * length is used to store the length of the profile
1401              * data only.
1402              */
1403             length -= ID_SIZE;
1404 
1405             // get the chunk number
1406             int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
1407             // get the total number of chunks
1408             numChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
1409 
1410             if (chunkNum > numChunks) {
1411                 throw new IIOException
1412                     ("Image format Error; chunk num > num chunks");
1413             }
1414 
1415             // if there are no more chunks, set up the data
1416             if (numChunks == 1) {
1417                 // reduce the stored length by the two chunk numbering bytes
1418                 length -= 2;
1419                 profile = new byte[length];
1420                 buffer.bufPtr += 2;
1421                 buffer.bufAvail-=2;
1422                 buffer.readData(profile);
1423                 inICC = false;
1424             } else {
1425                 // If we store them away, include the chunk numbering bytes
1426                 byte [] profileData = new byte[length];
1427                 // Now reduce the stored length by the
1428                 // two chunk numbering bytes
1429                 length -= 2;
1430                 buffer.readData(profileData);
1431                 chunks = new ArrayList();
1432                 chunks.add(profileData);
1433                 chunksRead = 1;
1434                 inICC = true;
1435             }
1436         }
1437 
1438         ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
1439             super(JPEG.APP2);
1440             if (node instanceof IIOMetadataNode) {
1441                 IIOMetadataNode ourNode = (IIOMetadataNode) node;
1442                 ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
1443                 if (prof != null) {  // May be null
1444                     profile = prof.getData();
1445                 }
1446             }
1447         }
1448 
1449         protected Object clone () {
1450             ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
1451             if (profile != null) {
1452                 newGuy.profile = profile.clone();
1453             }
1454             return newGuy;
1455         }
1456 
1457         boolean addData(JPEGBuffer buffer) throws IOException {
1458             if (debug) {
1459                 System.out.println("Adding to ICC segment");
1460             }
1461             // skip the tag
1462             buffer.bufPtr++;
1463             buffer.bufAvail--;
1464             // Get the length, but not in length
1465             int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
1466             dataLen |= buffer.buf[buffer.bufPtr++] & 0xff;
1467             buffer.bufAvail -= 2;
1468             // Don't include length itself
1469             dataLen -= 2;
1470             // skip the id
1471             buffer.bufPtr += ID_SIZE; // Skip the id
1472             buffer.bufAvail -= ID_SIZE;
1473             /*
1474              * Reduce the stored length by the id size.  The stored
1475              * length is used to store the length of the profile
1476              * data only.
1477              */
1478             dataLen -= ID_SIZE;
1479 
1480             // get the chunk number
1481             int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
1482             if (chunkNum > numChunks) {
1483                 throw new IIOException
1484                     ("Image format Error; chunk num > num chunks");
1485             }
1486 
1487             // get the number of chunks, which should match
1488             int newNumChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
1489             if (numChunks != newNumChunks) {
1490                 throw new IIOException
1491                     ("Image format Error; icc num chunks mismatch");
1492             }
1493             dataLen -= 2;
1494             if (debug) {
1495                 System.out.println("chunkNum: " + chunkNum
1496                                    + ", numChunks: " + numChunks
1497                                    + ", dataLen: " + dataLen);
1498             }
1499             boolean retval = false;
1500             byte [] profileData = new byte[dataLen];
1501             buffer.readData(profileData);
1502             chunks.add(profileData);
1503             length += dataLen;
1504             chunksRead++;
1505             if (chunksRead < numChunks) {
1506                 inICC = true;
1507             } else {
1508                 if (debug) {
1509                     System.out.println("Completing profile; total length is "
1510                                        + length);
1511                 }
1512                 // create an array for the whole thing
1513                 profile = new byte[length];
1514                 // copy the existing chunks, releasing them
1515                 // Note that they may be out of order
1516 
1517                 int index = 0;
1518                 for (int i = 1; i <= numChunks; i++) {
1519                     boolean foundIt = false;
1520                     for (int chunk = 0; chunk < chunks.size(); chunk++) {
1521                         byte [] chunkData = (byte []) chunks.get(chunk);
1522                         if (chunkData[0] == i) { // Right one
1523                             System.arraycopy(chunkData, 2,
1524                                              profile, index,
1525                                              chunkData.length-2);
1526                             index += chunkData.length-2;
1527                             foundIt = true;
1528                         }
1529                     }
1530                     if (foundIt == false) {
1531                         throw new IIOException
1532                             ("Image Format Error: Missing ICC chunk num " + i);
1533                     }
1534                 }
1535 
1536                 chunks = null;
1537                 chunksRead = 0;
1538                 numChunks = 0;
1539                 inICC = false;
1540                 retval = true;
1541             }
1542             return retval;
1543         }
1544 
1545         IIOMetadataNode getNativeNode() {
1546             IIOMetadataNode node = new IIOMetadataNode("app2ICC");
1547             if (profile != null) {
1548                 node.setUserObject(ICC_Profile.getInstance(profile));
1549             }
1550             return node;
1551         }
1552 
1553         /**
1554          * No-op.  Profiles are never written from metadata.
1555          * They are written from the ColorSpace of the image.
1556          */
1557         void write(ImageOutputStream ios) throws IOException {
1558             // No-op
1559         }
1560 
1561         void print () {
1562             printTag("ICC Profile APP2");
1563         }
1564     }
1565 }