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