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