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