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