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     private class IllegalThumbException extends Exception {}
 562 
 563     /**
 564      * Writes out a new JFXX extension segment, without saving it.
 565      */
 566     private void writeJFXXSegment(int index,
 567                                   BufferedImage thumbnail,
 568                                   ImageOutputStream ios,
 569                                   JPEGImageWriter writer) throws IOException {
 570         JFIFExtensionMarkerSegment jfxx = null;
 571         try {
 572              jfxx = new JFIFExtensionMarkerSegment(thumbnail);
 573         } catch (IllegalThumbException e) {
 574             writer.warningOccurred
 575                 (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
 576             return;
 577         }
 578         writer.thumbnailStarted(index);
 579         jfxx.write(ios, writer);
 580         writer.thumbnailComplete();
 581     }
 582 
 583 
 584     /**
 585      * Return an RGB image that is the expansion of the given grayscale
 586      * image.
 587      */
 588     private static BufferedImage expandGrayThumb(BufferedImage thumb) {
 589         BufferedImage ret = new BufferedImage(thumb.getWidth(),
 590                                               thumb.getHeight(),
 591                                               BufferedImage.TYPE_INT_RGB);
 592         Graphics g = ret.getGraphics();
 593         g.drawImage(thumb, 0, 0, null);
 594         return ret;
 595     }
 596 
 597     /**
 598      * Writes out a default JFIF marker segment to the given
 599      * output stream.  If <code>thumbnails</code> is not <code>null</code>,
 600      * writes out the set of thumbnail images as JFXX marker segments, or
 601      * incorporated into the JFIF segment if appropriate.
 602      * If <code>iccProfile</code> is not <code>null</code>,
 603      * writes out the profile after the JFIF segment using as many APP2
 604      * marker segments as necessary.
 605      */
 606     static void writeDefaultJFIF(ImageOutputStream ios,
 607                                  List thumbnails,
 608                                  ICC_Profile iccProfile,
 609                                  JPEGImageWriter writer)
 610         throws IOException {
 611 
 612         JFIFMarkerSegment jfif = new JFIFMarkerSegment();
 613         jfif.writeWithThumbs(ios, thumbnails, writer);
 614         if (iccProfile != null) {
 615             writeICC(iccProfile, ios);
 616         }
 617     }
 618 
 619     /**
 620      * Prints out the contents of this object to System.out for debugging.
 621      */
 622     void print() {
 623         printTag("JFIF");
 624         System.out.print("Version ");
 625         System.out.print(majorVersion);
 626         System.out.println(".0"
 627                            + Integer.toString(minorVersion));
 628         System.out.print("Resolution units: ");
 629         System.out.println(resUnits);
 630         System.out.print("X density: ");
 631         System.out.println(Xdensity);
 632         System.out.print("Y density: ");
 633         System.out.println(Ydensity);
 634         System.out.print("Thumbnail Width: ");
 635         System.out.println(thumbWidth);
 636         System.out.print("Thumbnail Height: ");
 637         System.out.println(thumbHeight);
 638         if (!extSegments.isEmpty()) {
 639             for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
 640                 JFIFExtensionMarkerSegment extSegment =
 641                     (JFIFExtensionMarkerSegment) iter.next();
 642                 extSegment.print();
 643             }
 644         }
 645         if (iccSegment != null) {
 646             iccSegment.print();
 647         }
 648     }
 649 
 650     /**
 651      * A JFIF extension APP0 marker segment.
 652      */
 653     class JFIFExtensionMarkerSegment extends MarkerSegment {
 654         int code;
 655         JFIFThumb thumb;
 656         private static final int DATA_SIZE = 6;
 657         private static final int ID_SIZE = 5;
 658 
 659         JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)
 660             throws IOException {
 661 
 662             super(buffer);
 663             buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it
 664 
 665             code = buffer.buf[buffer.bufPtr++] & 0xff;
 666             buffer.bufAvail -= DATA_SIZE;
 667             if (code == THUMB_JPEG) {
 668                 thumb = new JFIFThumbJPEG(buffer, length, reader);
 669             } else {
 670                 buffer.loadBuf(2);
 671                 int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
 672                 int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
 673                 buffer.bufAvail -= 2;
 674                 // following constructors handle bufAvail
 675                 if (code == THUMB_PALETTE) {
 676                     thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
 677                 } else {
 678                     thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
 679                 }
 680             }
 681         }
 682 
 683         JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
 684             super(JPEG.APP0);
 685             NamedNodeMap attrs = node.getAttributes();
 686             if (attrs.getLength() > 0) {
 687                 code = getAttributeValue(node,
 688                                          attrs,
 689                                          "extensionCode",
 690                                          THUMB_JPEG,
 691                                          THUMB_RGB,
 692                                          false);
 693                 if (code == THUMB_UNASSIGNED) {
 694                 throw new IIOInvalidTreeException
 695                     ("invalid extensionCode attribute value", node);
 696                 }
 697             } else {
 698                 code = THUMB_UNASSIGNED;
 699             }
 700             // Now the child
 701             if (node.getChildNodes().getLength() != 1) {
 702                 throw new IIOInvalidTreeException
 703                     ("app0JFXX node must have exactly 1 child", node);
 704             }
 705             Node child = node.getFirstChild();
 706             String name = child.getNodeName();
 707             if (name.equals("JFIFthumbJPEG")) {
 708                 if (code == THUMB_UNASSIGNED) {
 709                     code = THUMB_JPEG;
 710                 }
 711                 thumb = new JFIFThumbJPEG(child);
 712             } else if (name.equals("JFIFthumbPalette")) {
 713                 if (code == THUMB_UNASSIGNED) {
 714                     code = THUMB_PALETTE;
 715                 }
 716                 thumb = new JFIFThumbPalette(child);
 717             } else if (name.equals("JFIFthumbRGB")) {
 718                 if (code == THUMB_UNASSIGNED) {
 719                     code = THUMB_RGB;
 720                 }
 721                 thumb = new JFIFThumbRGB(child);
 722             } else {
 723                 throw new IIOInvalidTreeException
 724                     ("unrecognized app0JFXX child node", node);
 725             }
 726         }
 727 
 728         JFIFExtensionMarkerSegment(BufferedImage thumbnail)
 729             throws IllegalThumbException {
 730 
 731             super(JPEG.APP0);
 732             ColorModel cm = thumbnail.getColorModel();
 733             int csType = cm.getColorSpace().getType();
 734             if (cm.hasAlpha()) {
 735                 throw new IllegalThumbException();
 736             }
 737             if (cm instanceof IndexColorModel) {
 738                 code = THUMB_PALETTE;
 739                 thumb = new JFIFThumbPalette(thumbnail);
 740             } else if (csType == ColorSpace.TYPE_RGB) {
 741                 code = THUMB_RGB;
 742                 thumb = new JFIFThumbRGB(thumbnail);
 743             } else if (csType == ColorSpace.TYPE_GRAY) {
 744                 code = THUMB_JPEG;
 745                 thumb = new JFIFThumbJPEG(thumbnail);
 746             } else {
 747                 throw new IllegalThumbException();
 748             }
 749         }
 750 
 751         void setThumbnail(BufferedImage thumbnail) {
 752             try {
 753                 switch (code) {
 754                 case THUMB_PALETTE:
 755                     thumb = new JFIFThumbPalette(thumbnail);
 756                     break;
 757                 case THUMB_RGB:
 758                     thumb = new JFIFThumbRGB(thumbnail);
 759                     break;
 760                 case THUMB_JPEG:
 761                     thumb = new JFIFThumbJPEG(thumbnail);
 762                     break;
 763                 }
 764             } catch (IllegalThumbException e) {
 765                 // Should never happen
 766                 throw new InternalError("Illegal thumb in setThumbnail!", e);
 767             }
 768         }
 769 
 770         protected Object clone() {
 771             JFIFExtensionMarkerSegment newGuy =
 772                 (JFIFExtensionMarkerSegment) super.clone();
 773             if (thumb != null) {
 774                 newGuy.thumb = (JFIFThumb) thumb.clone();
 775             }
 776             return newGuy;
 777         }
 778 
 779         IIOMetadataNode getNativeNode() {
 780             IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
 781             node.setAttribute("extensionCode", Integer.toString(code));
 782             node.appendChild(thumb.getNativeNode());
 783             return node;
 784         }
 785 
 786         void write(ImageOutputStream ios,
 787                    JPEGImageWriter writer) throws IOException {
 788             length = LENGTH_SIZE + DATA_SIZE + thumb.getLength();
 789             writeTag(ios);
 790             byte [] id = {0x4A, 0x46, 0x58, 0x58, 0x00};
 791             ios.write(id);
 792             ios.write(code);
 793             thumb.write(ios, writer);
 794         }
 795 
 796         void print() {
 797             printTag("JFXX");
 798             thumb.print();
 799         }
 800     }
 801 
 802     /**
 803      * A superclass for the varieties of thumbnails that can
 804      * be stored in a JFIF extension marker segment.
 805      */
 806     abstract class JFIFThumb implements Cloneable {
 807         long streamPos = -1L;  // Save the thumbnail pos when reading
 808         abstract int getLength(); // When writing
 809         abstract int getWidth();
 810         abstract int getHeight();
 811         abstract BufferedImage getThumbnail(ImageInputStream iis,
 812                                             JPEGImageReader reader)
 813             throws IOException;
 814 
 815         protected JFIFThumb() {}
 816 
 817         protected JFIFThumb(JPEGBuffer buffer) throws IOException{
 818             // Save the stream position for reading the thumbnail later
 819             streamPos = buffer.getStreamPosition();
 820         }
 821 
 822         abstract void print();
 823 
 824         abstract IIOMetadataNode getNativeNode();
 825 
 826         abstract void write(ImageOutputStream ios,
 827                             JPEGImageWriter writer) throws IOException;
 828 
 829         protected Object clone() {
 830             try {
 831                 return super.clone();
 832             } catch (CloneNotSupportedException e) {} // won't happen
 833             return null;
 834         }
 835 
 836     }
 837 
 838     abstract class JFIFThumbUncompressed extends JFIFThumb {
 839         BufferedImage thumbnail = null;
 840         int thumbWidth;
 841         int thumbHeight;
 842         String name;
 843 
 844         JFIFThumbUncompressed(JPEGBuffer buffer,
 845                               int width,
 846                               int height,
 847                               int skip,
 848                               String name)
 849             throws IOException {
 850             super(buffer);
 851             thumbWidth = width;
 852             thumbHeight = height;
 853             // Now skip the thumbnail data
 854             buffer.skipData(skip);
 855             this.name = name;
 856         }
 857 
 858         JFIFThumbUncompressed(Node node, String name)
 859             throws IIOInvalidTreeException {
 860 
 861             thumbWidth = 0;
 862             thumbHeight = 0;
 863             this.name = name;
 864             NamedNodeMap attrs = node.getAttributes();
 865             int count = attrs.getLength();
 866             if (count > 2) {
 867                 throw new IIOInvalidTreeException
 868                     (name +" node cannot have > 2 attributes", node);
 869             }
 870             if (count != 0) {
 871                 int value = getAttributeValue(node, attrs, "thumbWidth",
 872                                               0, 255, false);
 873                 thumbWidth = (value != -1) ? value : thumbWidth;
 874                 value = getAttributeValue(node, attrs, "thumbHeight",
 875                                           0, 255, false);
 876                 thumbHeight = (value != -1) ? value : thumbHeight;
 877             }
 878         }
 879 
 880         JFIFThumbUncompressed(BufferedImage thumb) {
 881             thumbnail = thumb;
 882             thumbWidth = thumb.getWidth();
 883             thumbHeight = thumb.getHeight();
 884             name = null;  // not used when writing
 885         }
 886 
 887         void readByteBuffer(ImageInputStream iis,
 888                             byte [] data,
 889                             JPEGImageReader reader,
 890                             float workPortion,
 891                             float workOffset) throws IOException {
 892             int progInterval = Math.max((int)(data.length/20/workPortion),
 893                                         1);
 894             for (int offset = 0;
 895                  offset < data.length;) {
 896                 int len = Math.min(progInterval, data.length-offset);
 897                 iis.read(data, offset, len);
 898                 offset += progInterval;
 899                 float percentDone = ((float) offset* 100)
 900                     / data.length
 901                     * workPortion + workOffset;
 902                 if (percentDone > 100.0F) {
 903                     percentDone = 100.0F;
 904                 }
 905                 reader.thumbnailProgress (percentDone);
 906             }
 907         }
 908 
 909 
 910         int getWidth() {
 911             return thumbWidth;
 912         }
 913 
 914         int getHeight() {
 915             return thumbHeight;
 916         }
 917 
 918         IIOMetadataNode getNativeNode() {
 919             IIOMetadataNode node = new IIOMetadataNode(name);
 920             node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
 921             node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
 922             return node;
 923         }
 924 
 925         void write(ImageOutputStream ios,
 926                    JPEGImageWriter writer) throws IOException {
 927             if ((thumbWidth > MAX_THUMB_WIDTH)
 928                 || (thumbHeight > MAX_THUMB_HEIGHT)) {
 929                 writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
 930             }
 931             thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
 932             thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
 933             ios.write(thumbWidth);
 934             ios.write(thumbHeight);
 935         }
 936 
 937         void writePixels(ImageOutputStream ios,
 938                          JPEGImageWriter writer) throws IOException {
 939             if ((thumbWidth > MAX_THUMB_WIDTH)
 940                 || (thumbHeight > MAX_THUMB_HEIGHT)) {
 941                 writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
 942             }
 943             thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
 944             thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
 945             int [] data = thumbnail.getRaster().getPixels(0, 0,
 946                                                           thumbWidth,
 947                                                           thumbHeight,
 948                                                           (int []) null);
 949             writeThumbnailData(ios, data, writer);
 950         }
 951 
 952         void print() {
 953             System.out.print(name + " width: ");
 954             System.out.println(thumbWidth);
 955             System.out.print(name + " height: ");
 956             System.out.println(thumbHeight);
 957         }
 958 
 959     }
 960 
 961     /**
 962      * A JFIF thumbnail stored as RGB, one byte per channel,
 963      * interleaved.
 964      */
 965     class JFIFThumbRGB extends JFIFThumbUncompressed {
 966 
 967         JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
 968             throws IOException {
 969 
 970             super(buffer, width, height, width*height*3, "JFIFthumbRGB");
 971         }
 972 
 973         JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
 974             super(node, "JFIFthumbRGB");
 975         }
 976 
 977         JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
 978             super(thumb);
 979         }
 980 
 981         int getLength() {
 982             return (thumbWidth*thumbHeight*3);
 983         }
 984 
 985         BufferedImage getThumbnail(ImageInputStream iis,
 986                                    JPEGImageReader reader)
 987             throws IOException {
 988             iis.mark();
 989             iis.seek(streamPos);
 990             DataBufferByte buffer = new DataBufferByte(getLength());
 991             readByteBuffer(iis,
 992                            buffer.getData(),
 993                            reader,
 994                            1.0F,
 995                            0.0F);
 996             iis.reset();
 997 
 998             WritableRaster raster =
 999                 Raster.createInterleavedRaster(buffer,
1000                                                thumbWidth,
1001                                                thumbHeight,
1002                                                thumbWidth*3,
1003                                                3,
1004                                                new int [] {0, 1, 2},
1005                                                null);
1006             ColorModel cm = new ComponentColorModel(JPEG.JCS.sRGB,
1007                                                     false,
1008                                                     false,
1009                                                     ColorModel.OPAQUE,
1010                                                     DataBuffer.TYPE_BYTE);
1011             return new BufferedImage(cm,
1012                                      raster,
1013                                      false,
1014                                      null);
1015         }
1016 
1017         void write(ImageOutputStream ios,
1018                    JPEGImageWriter writer) throws IOException {
1019             super.write(ios, writer); // width and height
1020             writePixels(ios, writer);
1021         }
1022 
1023     }
1024 
1025     /**
1026      * A JFIF thumbnail stored as an indexed palette image
1027      * using an RGB palette.
1028      */
1029     class JFIFThumbPalette extends JFIFThumbUncompressed {
1030         private static final int PALETTE_SIZE = 768;
1031 
1032         JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
1033             throws IOException {
1034             super(buffer,
1035                   width,
1036                   height,
1037                   PALETTE_SIZE + width * height,
1038                   "JFIFThumbPalette");
1039         }
1040 
1041         JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
1042             super(node, "JFIFThumbPalette");
1043         }
1044 
1045         JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException {
1046             super(thumb);
1047             IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
1048             if (icm.getMapSize() > 256) {
1049                 throw new IllegalThumbException();
1050             }
1051         }
1052 
1053         int getLength() {
1054             return (thumbWidth*thumbHeight + PALETTE_SIZE);
1055         }
1056 
1057         BufferedImage getThumbnail(ImageInputStream iis,
1058                                    JPEGImageReader reader)
1059             throws IOException {
1060             iis.mark();
1061             iis.seek(streamPos);
1062             // read the palette
1063             byte [] palette = new byte [PALETTE_SIZE];
1064             float palettePart = ((float) PALETTE_SIZE) / getLength();
1065             readByteBuffer(iis,
1066                            palette,
1067                            reader,
1068                            palettePart,
1069                            0.0F);
1070             DataBufferByte buffer = new DataBufferByte(thumbWidth*thumbHeight);
1071             readByteBuffer(iis,
1072                            buffer.getData(),
1073                            reader,
1074                            1.0F-palettePart,
1075                            palettePart);
1076             iis.read();
1077             iis.reset();
1078 
1079             IndexColorModel cm = new IndexColorModel(8,
1080                                                      256,
1081                                                      palette,
1082                                                      0,
1083                                                      false);
1084             SampleModel sm = cm.createCompatibleSampleModel(thumbWidth,
1085                                                             thumbHeight);
1086             WritableRaster raster =
1087                 Raster.createWritableRaster(sm, buffer, null);
1088             return new BufferedImage(cm,
1089                                      raster,
1090                                      false,
1091                                      null);
1092         }
1093 
1094         void write(ImageOutputStream ios,
1095                    JPEGImageWriter writer) throws IOException {
1096             super.write(ios, writer); // width and height
1097             // Write the palette (must be 768 bytes)
1098             byte [] palette = new byte[768];
1099             IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
1100             byte [] reds = new byte [256];
1101             byte [] greens = new byte [256];
1102             byte [] blues = new byte [256];
1103             icm.getReds(reds);
1104             icm.getGreens(greens);
1105             icm.getBlues(blues);
1106             for (int i = 0; i < 256; i++) {
1107                 palette[i*3] = reds[i];
1108                 palette[i*3+1] = greens[i];
1109                 palette[i*3+2] = blues[i];
1110             }
1111             ios.write(palette);
1112             writePixels(ios, writer);
1113         }
1114     }
1115 
1116 
1117     /**
1118      * A JFIF thumbnail stored as a JPEG stream.  No JFIF or
1119      * JFIF extension markers are permitted.  There is no need
1120      * to clip these, but the entire image must fit into a
1121      * single JFXX marker segment.
1122      */
1123     class JFIFThumbJPEG extends JFIFThumb {
1124         JPEGMetadata thumbMetadata = null;
1125         byte [] data = null;  // Compressed image data, for writing
1126         private static final int PREAMBLE_SIZE = 6;
1127 
1128         JFIFThumbJPEG(JPEGBuffer buffer,
1129                       int length,
1130                       JPEGImageReader reader) throws IOException {
1131             super(buffer);
1132             // Compute the final stream position
1133             long finalPos = streamPos + (length - PREAMBLE_SIZE);
1134             // Set the stream back to the start of the thumbnail
1135             // and read its metadata (but don't decode the image)
1136             buffer.iis.seek(streamPos);
1137             thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
1138             // Set the stream to the computed final position
1139             buffer.iis.seek(finalPos);
1140             // Clear the now invalid buffer
1141             buffer.bufAvail = 0;
1142             buffer.bufPtr = 0;
1143         }
1144 
1145         JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
1146             if (node.getChildNodes().getLength() > 1) {
1147                 throw new IIOInvalidTreeException
1148                     ("JFIFThumbJPEG node must have 0 or 1 child", node);
1149             }
1150             Node child = node.getFirstChild();
1151             if (child != null) {
1152                 String name = child.getNodeName();
1153                 if (!name.equals("markerSequence")) {
1154                     throw new IIOInvalidTreeException
1155                         ("JFIFThumbJPEG child must be a markerSequence node",
1156                          node);
1157                 }
1158                 thumbMetadata = new JPEGMetadata(false, true);
1159                 thumbMetadata.setFromMarkerSequenceNode(child);
1160             }
1161         }
1162 
1163         JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException {
1164             int INITIAL_BUFSIZE = 4096;
1165             int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE;
1166             try {
1167                 ByteArrayOutputStream baos =
1168                     new ByteArrayOutputStream(INITIAL_BUFSIZE);
1169                 MemoryCacheImageOutputStream mos =
1170                     new MemoryCacheImageOutputStream(baos);
1171 
1172                 JPEGImageWriter thumbWriter = new JPEGImageWriter(null);
1173 
1174                 thumbWriter.setOutput(mos);
1175 
1176                 // get default metadata for the thumb
1177                 JPEGMetadata metadata =
1178                     (JPEGMetadata) thumbWriter.getDefaultImageMetadata
1179                     (new ImageTypeSpecifier(thumb), null);
1180 
1181                 // Remove the jfif segment, which should be there.
1182                 MarkerSegment jfif = metadata.findMarkerSegment
1183                     (JFIFMarkerSegment.class, true);
1184                 if (jfif == null) {
1185                     throw new IllegalThumbException();
1186                 }
1187 
1188                 metadata.markerSequence.remove(jfif);
1189 
1190                 /*  Use this if removing leaves a hole and causes trouble
1191 
1192                 // Get the tree
1193                 String format = metadata.getNativeMetadataFormatName();
1194                 IIOMetadataNode tree =
1195                 (IIOMetadataNode) metadata.getAsTree(format);
1196 
1197                 // If there is no app0jfif node, the image is bad
1198                 NodeList jfifs = tree.getElementsByTagName("app0JFIF");
1199                 if (jfifs.getLength() == 0) {
1200                 throw new IllegalThumbException();
1201                 }
1202 
1203                 // remove the app0jfif node
1204                 Node jfif = jfifs.item(0);
1205                 Node parent = jfif.getParentNode();
1206                 parent.removeChild(jfif);
1207 
1208                 metadata.setFromTree(format, tree);
1209                 */
1210 
1211                 thumbWriter.write(new IIOImage(thumb, null, metadata));
1212 
1213                 thumbWriter.dispose();
1214                 // Now check that the size is OK
1215                 if (baos.size() > MAZ_BUFSIZE) {
1216                     throw new IllegalThumbException();
1217                 }
1218                 data = baos.toByteArray();
1219             } catch (IOException e) {
1220                 throw new IllegalThumbException();
1221             }
1222         }
1223 
1224         int getWidth() {
1225             int retval = 0;
1226             SOFMarkerSegment sof =
1227                 (SOFMarkerSegment) thumbMetadata.findMarkerSegment
1228                 (SOFMarkerSegment.class, true);
1229             if (sof != null) {
1230                 retval = sof.samplesPerLine;
1231             }
1232             return retval;
1233         }
1234 
1235         int getHeight() {
1236             int retval = 0;
1237             SOFMarkerSegment sof =
1238                 (SOFMarkerSegment) thumbMetadata.findMarkerSegment
1239                 (SOFMarkerSegment.class, true);
1240             if (sof != null) {
1241                 retval = sof.numLines;
1242             }
1243             return retval;
1244         }
1245 
1246         private class ThumbnailReadListener
1247             implements IIOReadProgressListener {
1248             JPEGImageReader reader = null;
1249             ThumbnailReadListener (JPEGImageReader reader) {
1250                 this.reader = reader;
1251             }
1252             public void sequenceStarted(ImageReader source, int minIndex) {}
1253             public void sequenceComplete(ImageReader source) {}
1254             public void imageStarted(ImageReader source, int imageIndex) {}
1255             public void imageProgress(ImageReader source,
1256                                       float percentageDone) {
1257                 reader.thumbnailProgress(percentageDone);
1258             }
1259             public void imageComplete(ImageReader source) {}
1260             public void thumbnailStarted(ImageReader source,
1261                 int imageIndex, int thumbnailIndex) {}
1262             public void thumbnailProgress(ImageReader source, float percentageDone) {}
1263             public void thumbnailComplete(ImageReader source) {}
1264             public void readAborted(ImageReader source) {}
1265         }
1266 
1267         BufferedImage getThumbnail(ImageInputStream iis,
1268                                    JPEGImageReader reader)
1269             throws IOException {
1270             iis.mark();
1271             iis.seek(streamPos);
1272             JPEGImageReader thumbReader = new JPEGImageReader(null);
1273             thumbReader.setInput(iis);
1274             thumbReader.addIIOReadProgressListener
1275                 (new ThumbnailReadListener(reader));
1276             BufferedImage ret = thumbReader.read(0, null);
1277             thumbReader.dispose();
1278             iis.reset();
1279             return ret;
1280         }
1281 
1282         protected Object clone() {
1283             JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
1284             if (thumbMetadata != null) {
1285                 newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
1286             }
1287             return newGuy;
1288         }
1289 
1290         IIOMetadataNode getNativeNode() {
1291             IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
1292             if (thumbMetadata != null) {
1293                 node.appendChild(thumbMetadata.getNativeTree());
1294             }
1295             return node;
1296         }
1297 
1298         int getLength() {
1299             if (data == null) {
1300                 return 0;
1301             } else {
1302                 return data.length;
1303             }
1304         }
1305 
1306         void write(ImageOutputStream ios,
1307                    JPEGImageWriter writer) throws IOException {
1308             int progInterval = data.length / 20;  // approx. every 5%
1309             if (progInterval == 0) {
1310                 progInterval = 1;
1311             }
1312             for (int offset = 0;
1313                  offset < data.length;) {
1314                 int len = Math.min(progInterval, data.length-offset);
1315                 ios.write(data, offset, len);
1316                 offset += progInterval;
1317                 float percentDone = ((float) offset * 100) / data.length;
1318                 if (percentDone > 100.0F) {
1319                     percentDone = 100.0F;
1320                 }
1321                 writer.thumbnailProgress (percentDone);
1322             }
1323         }
1324 
1325         void print () {
1326             System.out.println("JFIF thumbnail stored as JPEG");
1327         }
1328     }
1329 
1330     /**
1331      * Write out the given profile to the stream, embedded in
1332      * the necessary number of APP2 segments, per the ICC spec.
1333      * This is the only mechanism for writing an ICC profile
1334      * to a stream.
1335      */
1336     static void writeICC(ICC_Profile profile, ImageOutputStream ios)
1337         throws IOException {
1338         int LENGTH_LENGTH = 2;
1339         final String ID = "ICC_PROFILE";
1340         int ID_LENGTH = ID.length()+1; // spec says it's null-terminated
1341         int COUNTS_LENGTH = 2;
1342         int MAX_ICC_CHUNK_SIZE =
1343             65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH;
1344 
1345         byte [] data = profile.getData();
1346         int numChunks = data.length / MAX_ICC_CHUNK_SIZE;
1347         if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) {
1348             numChunks++;
1349         }
1350         int chunkNum = 1;
1351         int offset = 0;
1352         for (int i = 0; i < numChunks; i++) {
1353             int dataLength = Math.min(data.length-offset, MAX_ICC_CHUNK_SIZE);
1354             int segLength = dataLength+COUNTS_LENGTH+ID_LENGTH+LENGTH_LENGTH;
1355             ios.write(0xff);
1356             ios.write(JPEG.APP2);
1357             MarkerSegment.write2bytes(ios, segLength);
1358             byte [] id = ID.getBytes("US-ASCII");
1359             ios.write(id);
1360             ios.write(0); // Null-terminate the string
1361             ios.write(chunkNum++);
1362             ios.write(numChunks);
1363             ios.write(data, offset, dataLength);
1364             offset += dataLength;
1365         }
1366     }
1367 
1368     /**
1369      * An APP2 marker segment containing an ICC profile.  In the stream
1370      * a profile larger than 64K is broken up into a series of chunks.
1371      * This inner class represents the complete profile as a single object,
1372      * combining chunks as necessary.
1373      */
1374     class ICCMarkerSegment extends MarkerSegment {
1375         ArrayList chunks = null;
1376         byte [] profile = null; // The complete profile when it's fully read
1377                          // May remain null when writing
1378         private static final int ID_SIZE = 12;
1379         int chunksRead;
1380         int numChunks;
1381 
1382         ICCMarkerSegment(ICC_ColorSpace cs) {
1383             super(JPEG.APP2);
1384             chunks = null;
1385             chunksRead = 0;
1386             numChunks = 0;
1387             profile = cs.getProfile().getData();
1388         }
1389 
1390         ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
1391             super(buffer);  // gets whole segment or fills the buffer
1392             if (debug) {
1393                 System.out.println("Creating new ICC segment");
1394             }
1395             buffer.bufPtr += ID_SIZE; // Skip the id
1396             buffer.bufAvail -= ID_SIZE;
1397             /*
1398              * Reduce the stored length by the id size.  The stored
1399              * length is used to store the length of the profile
1400              * data only.
1401              */
1402             length -= ID_SIZE;
1403 
1404             // get the chunk number
1405             int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
1406             // get the total number of chunks
1407             numChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
1408 
1409             if (chunkNum > numChunks) {
1410                 throw new IIOException
1411                     ("Image format Error; chunk num > num chunks");
1412             }
1413 
1414             // if there are no more chunks, set up the data
1415             if (numChunks == 1) {
1416                 // reduce the stored length by the two chunk numbering bytes
1417                 length -= 2;
1418                 profile = new byte[length];
1419                 buffer.bufPtr += 2;
1420                 buffer.bufAvail-=2;
1421                 buffer.readData(profile);
1422                 inICC = false;
1423             } else {
1424                 // If we store them away, include the chunk numbering bytes
1425                 byte [] profileData = new byte[length];
1426                 // Now reduce the stored length by the
1427                 // two chunk numbering bytes
1428                 length -= 2;
1429                 buffer.readData(profileData);
1430                 chunks = new ArrayList();
1431                 chunks.add(profileData);
1432                 chunksRead = 1;
1433                 inICC = true;
1434             }
1435         }
1436 
1437         ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
1438             super(JPEG.APP2);
1439             if (node instanceof IIOMetadataNode) {
1440                 IIOMetadataNode ourNode = (IIOMetadataNode) node;
1441                 ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
1442                 if (prof != null) {  // May be null
1443                     profile = prof.getData();
1444                 }
1445             }
1446         }
1447 
1448         protected Object clone () {
1449             ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
1450             if (profile != null) {
1451                 newGuy.profile = profile.clone();
1452             }
1453             return newGuy;
1454         }
1455 
1456         boolean addData(JPEGBuffer buffer) throws IOException {
1457             if (debug) {
1458                 System.out.println("Adding to ICC segment");
1459             }
1460             // skip the tag
1461             buffer.bufPtr++;
1462             buffer.bufAvail--;
1463             // Get the length, but not in length
1464             int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
1465             dataLen |= buffer.buf[buffer.bufPtr++] & 0xff;
1466             buffer.bufAvail -= 2;
1467             // Don't include length itself
1468             dataLen -= 2;
1469             // skip the id
1470             buffer.bufPtr += ID_SIZE; // Skip the id
1471             buffer.bufAvail -= ID_SIZE;
1472             /*
1473              * Reduce the stored length by the id size.  The stored
1474              * length is used to store the length of the profile
1475              * data only.
1476              */
1477             dataLen -= ID_SIZE;
1478 
1479             // get the chunk number
1480             int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
1481             if (chunkNum > numChunks) {
1482                 throw new IIOException
1483                     ("Image format Error; chunk num > num chunks");
1484             }
1485 
1486             // get the number of chunks, which should match
1487             int newNumChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
1488             if (numChunks != newNumChunks) {
1489                 throw new IIOException
1490                     ("Image format Error; icc num chunks mismatch");
1491             }
1492             dataLen -= 2;
1493             if (debug) {
1494                 System.out.println("chunkNum: " + chunkNum
1495                                    + ", numChunks: " + numChunks
1496                                    + ", dataLen: " + dataLen);
1497             }
1498             boolean retval = false;
1499             byte [] profileData = new byte[dataLen];
1500             buffer.readData(profileData);
1501             chunks.add(profileData);
1502             length += dataLen;
1503             chunksRead++;
1504             if (chunksRead < numChunks) {
1505                 inICC = true;
1506             } else {
1507                 if (debug) {
1508                     System.out.println("Completing profile; total length is "
1509                                        + length);
1510                 }
1511                 // create an array for the whole thing
1512                 profile = new byte[length];
1513                 // copy the existing chunks, releasing them
1514                 // Note that they may be out of order
1515 
1516                 int index = 0;
1517                 for (int i = 1; i <= numChunks; i++) {
1518                     boolean foundIt = false;
1519                     for (int chunk = 0; chunk < chunks.size(); chunk++) {
1520                         byte [] chunkData = (byte []) chunks.get(chunk);
1521                         if (chunkData[0] == i) { // Right one
1522                             System.arraycopy(chunkData, 2,
1523                                              profile, index,
1524                                              chunkData.length-2);
1525                             index += chunkData.length-2;
1526                             foundIt = true;
1527                         }
1528                     }
1529                     if (foundIt == false) {
1530                         throw new IIOException
1531                             ("Image Format Error: Missing ICC chunk num " + i);
1532                     }
1533                 }
1534 
1535                 chunks = null;
1536                 chunksRead = 0;
1537                 numChunks = 0;
1538                 inICC = false;
1539                 retval = true;
1540             }
1541             return retval;
1542         }
1543 
1544         IIOMetadataNode getNativeNode() {
1545             IIOMetadataNode node = new IIOMetadataNode("app2ICC");
1546             if (profile != null) {
1547                 node.setUserObject(ICC_Profile.getInstance(profile));
1548             }
1549             return node;
1550         }
1551 
1552         /**
1553          * No-op.  Profiles are never written from metadata.
1554          * They are written from the ColorSpace of the image.
1555          */
1556         void write(ImageOutputStream ios) throws IOException {
1557             // No-op
1558         }
1559 
1560         void print () {
1561             printTag("ICC Profile APP2");
1562         }
1563     }
1564 }