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