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