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