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} 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      */
JFIFMarkerSegment()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      */
JFIFMarkerSegment(JPEGBuffer buffer)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      */
JFIFMarkerSegment(Node node)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      */
clone()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      */
addJFXX(JPEGBuffer buffer, JPEGImageReader reader)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      */
addICC(JPEGBuffer buffer)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      */
addICC(ICC_ColorSpace cs)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      */
getNativeNode()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      */
updateFromNativeNode(Node node, boolean fromScratch)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 
getThumbnailWidth(int index)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 
getThumbnailHeight(int index)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 
getThumbnail(ImageInputStream iis, int index, JPEGImageReader reader)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      */
write(ImageOutputStream ios, JPEGImageWriter writer)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      */
write(ImageOutputStream ios, BufferedImage thumb, JPEGImageWriter writer)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      */
writeThumbnailData(ImageOutputStream ios, int [] thumbData, JPEGImageWriter writer)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      */
writeWithThumbs(ImageOutputStream ios, List<? extends BufferedImage> thumbnails, JPEGImageWriter writer)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 
writeThumb(ImageOutputStream ios, BufferedImage thumb, JFIFExtensionMarkerSegment jfxx, int index, boolean onlyOne, JPEGImageWriter writer)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      */
writeJFXXSegment(int index, BufferedImage thumbnail, ImageOutputStream ios, JPEGImageWriter writer)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      */
expandGrayThumb(BufferedImage thumb)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} is not {@code null},
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} is not {@code null},
601      * writes out the profile after the JFIF segment using as many APP2
602      * marker segments as necessary.
603      */
writeDefaultJFIF(ImageOutputStream ios, List<? extends BufferedImage> thumbnails, ICC_Profile iccProfile, JPEGImageWriter writer)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      */
print()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 
JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)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 
JFIFExtensionMarkerSegment(Node node)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 
JFIFExtensionMarkerSegment(BufferedImage thumbnail)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 
setThumbnail(BufferedImage thumbnail)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 
clone()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 
getNativeNode()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 
write(ImageOutputStream ios, JPEGImageWriter writer)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 
print()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
getLength()806         abstract int getLength(); // When writing
getWidth()807         abstract int getWidth();
getHeight()808         abstract int getHeight();
getThumbnail(ImageInputStream iis, JPEGImageReader reader)809         abstract BufferedImage getThumbnail(ImageInputStream iis,
810                                             JPEGImageReader reader)
811             throws IOException;
812 
JFIFThumb()813         protected JFIFThumb() {}
814 
JFIFThumb(JPEGBuffer buffer)815         protected JFIFThumb(JPEGBuffer buffer) throws IOException{
816             // Save the stream position for reading the thumbnail later
817             streamPos = buffer.getStreamPosition();
818         }
819 
print()820         abstract void print();
821 
getNativeNode()822         abstract IIOMetadataNode getNativeNode();
823 
write(ImageOutputStream ios, JPEGImageWriter writer)824         abstract void write(ImageOutputStream ios,
825                             JPEGImageWriter writer) throws IOException;
826 
clone()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 
JFIFThumbUncompressed(JPEGBuffer buffer, int width, int height, int skip, String name)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 
JFIFThumbUncompressed(Node node, String name)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 
JFIFThumbUncompressed(BufferedImage thumb)878         JFIFThumbUncompressed(BufferedImage thumb) {
879             thumbnail = thumb;
880             thumbWidth = thumb.getWidth();
881             thumbHeight = thumb.getHeight();
882             name = null;  // not used when writing
883         }
884 
readByteBuffer(ImageInputStream iis, byte [] data, JPEGImageReader reader, float workPortion, float workOffset)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 
getWidth()908         int getWidth() {
909             return thumbWidth;
910         }
911 
getHeight()912         int getHeight() {
913             return thumbHeight;
914         }
915 
getNativeNode()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 
write(ImageOutputStream ios, JPEGImageWriter writer)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 
writePixels(ImageOutputStream ios, JPEGImageWriter writer)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 
print()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 
JFIFThumbRGB(JPEGBuffer buffer, int width, int height)965         JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
966             throws IOException {
967 
968             super(buffer, width, height, width*height*3, "JFIFthumbRGB");
969         }
970 
JFIFThumbRGB(Node node)971         JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
972             super(node, "JFIFthumbRGB");
973         }
974 
JFIFThumbRGB(BufferedImage thumb)975         JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
976             super(thumb);
977         }
978 
getLength()979         int getLength() {
980             return (thumbWidth*thumbHeight*3);
981         }
982 
getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 
write(ImageOutputStream ios, JPEGImageWriter writer)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 
JFIFThumbPalette(JPEGBuffer buffer, int width, int height)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 
JFIFThumbPalette(Node node)1039         JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
1040             super(node, "JFIFThumbPalette");
1041         }
1042 
JFIFThumbPalette(BufferedImage thumb)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 
getLength()1051         int getLength() {
1052             return (thumbWidth*thumbHeight + PALETTE_SIZE);
1053         }
1054 
getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 
write(ImageOutputStream ios, JPEGImageWriter writer)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 
JFIFThumbJPEG(JPEGBuffer buffer, int length, JPEGImageReader reader)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 
JFIFThumbJPEG(Node node)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 
JFIFThumbJPEG(BufferedImage thumb)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 
getWidth()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 
getHeight()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;
ThumbnailReadListener(JPEGImageReader reader)1247             ThumbnailReadListener (JPEGImageReader reader) {
1248                 this.reader = reader;
1249             }
sequenceStarted(ImageReader source, int minIndex)1250             public void sequenceStarted(ImageReader source, int minIndex) {}
sequenceComplete(ImageReader source)1251             public void sequenceComplete(ImageReader source) {}
imageStarted(ImageReader source, int imageIndex)1252             public void imageStarted(ImageReader source, int imageIndex) {}
imageProgress(ImageReader source, float percentageDone)1253             public void imageProgress(ImageReader source,
1254                                       float percentageDone) {
1255                 reader.thumbnailProgress(percentageDone);
1256             }
imageComplete(ImageReader source)1257             public void imageComplete(ImageReader source) {}
thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex)1258             public void thumbnailStarted(ImageReader source,
1259                 int imageIndex, int thumbnailIndex) {}
thumbnailProgress(ImageReader source, float percentageDone)1260             public void thumbnailProgress(ImageReader source, float percentageDone) {}
thumbnailComplete(ImageReader source)1261             public void thumbnailComplete(ImageReader source) {}
readAborted(ImageReader source)1262             public void readAborted(ImageReader source) {}
1263         }
1264 
getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 
clone()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 
getNativeNode()1288         IIOMetadataNode getNativeNode() {
1289             IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
1290             if (thumbMetadata != null) {
1291                 node.appendChild(thumbMetadata.getNativeTree());
1292             }
1293             return node;
1294         }
1295 
getLength()1296         int getLength() {
1297             if (data == null) {
1298                 return 0;
1299             } else {
1300                 return data.length;
1301             }
1302         }
1303 
write(ImageOutputStream ios, JPEGImageWriter writer)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 
print()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      */
writeICC(ICC_Profile profile, ImageOutputStream ios)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 
ICCMarkerSegment(ICC_ColorSpace cs)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 
ICCMarkerSegment(JPEGBuffer buffer)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 
ICCMarkerSegment(Node node)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 
clone()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 
addData(JPEGBuffer buffer)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 
getNativeNode()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          */
write(ImageOutputStream ios)1554         void write(ImageOutputStream ios) throws IOException {
1555             // No-op
1556         }
1557 
print()1558         void print () {
1559             printTag("ICC Profile APP2");
1560         }
1561     }
1562 }
1563