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