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} 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 */ JFIFMarkerSegment()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 */ JFIFMarkerSegment(JPEGBuffer buffer)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 */ JFIFMarkerSegment(Node node)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 */ clone()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 */ addJFXX(JPEGBuffer buffer, JPEGImageReader reader)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 */ addICC(JPEGBuffer buffer)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 */ addICC(ICC_ColorSpace cs)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 */ getNativeNode()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 */ updateFromNativeNode(Node node, boolean fromScratch)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 getThumbnailWidth(int index)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 getThumbnailHeight(int index)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 getThumbnail(ImageInputStream iis, int index, JPEGImageReader reader)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 */ write(ImageOutputStream ios, JPEGImageWriter writer)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 */ write(ImageOutputStream ios, BufferedImage thumb, JPEGImageWriter writer)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 */ writeThumbnailData(ImageOutputStream ios, int [] thumbData, JPEGImageWriter writer)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 */ writeWithThumbs(ImageOutputStream ios, List<? extends BufferedImage> thumbnails, JPEGImageWriter writer)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 writeThumb(ImageOutputStream ios, BufferedImage thumb, JFIFExtensionMarkerSegment jfxx, int index, boolean onlyOne, JPEGImageWriter writer)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 */ writeJFXXSegment(int index, BufferedImage thumbnail, ImageOutputStream ios, JPEGImageWriter writer)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 */ expandGrayThumb(BufferedImage thumb)586 private static BufferedImage expandGrayThumb(BufferedImage thumb) { 587 BufferedImage ret = new BufferedImage(thumb.getWidth(), 588 thumb.getHeight(), 589 BufferedImage.TYPE_INT_RGB); 590 Graphics g = ret.getGraphics(); 591 g.drawImage(thumb, 0, 0, null); 592 return ret; 593 } 594 595 /** 596 * Writes out a default JFIF marker segment to the given 597 * output stream. If {@code thumbnails} is not {@code null}, 598 * writes out the set of thumbnail images as JFXX marker segments, or 599 * incorporated into the JFIF segment if appropriate. 600 * If {@code iccProfile} is not {@code null}, 601 * writes out the profile after the JFIF segment using as many APP2 602 * marker segments as necessary. 603 */ writeDefaultJFIF(ImageOutputStream ios, List<? extends BufferedImage> thumbnails, ICC_Profile iccProfile, JPEGImageWriter writer)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 */ print()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 JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)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 JFIFExtensionMarkerSegment(Node node)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 JFIFExtensionMarkerSegment(BufferedImage thumbnail)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 setThumbnail(BufferedImage thumbnail)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 clone()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 getNativeNode()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 write(ImageOutputStream ios, JPEGImageWriter writer)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 print()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 getLength()806 abstract int getLength(); // When writing getWidth()807 abstract int getWidth(); getHeight()808 abstract int getHeight(); getThumbnail(ImageInputStream iis, JPEGImageReader reader)809 abstract BufferedImage getThumbnail(ImageInputStream iis, 810 JPEGImageReader reader) 811 throws IOException; 812 JFIFThumb()813 protected JFIFThumb() {} 814 JFIFThumb(JPEGBuffer buffer)815 protected JFIFThumb(JPEGBuffer buffer) throws IOException{ 816 // Save the stream position for reading the thumbnail later 817 streamPos = buffer.getStreamPosition(); 818 } 819 print()820 abstract void print(); 821 getNativeNode()822 abstract IIOMetadataNode getNativeNode(); 823 write(ImageOutputStream ios, JPEGImageWriter writer)824 abstract void write(ImageOutputStream ios, 825 JPEGImageWriter writer) throws IOException; 826 clone()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 JFIFThumbUncompressed(JPEGBuffer buffer, int width, int height, int skip, String name)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 JFIFThumbUncompressed(Node node, String name)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 JFIFThumbUncompressed(BufferedImage thumb)878 JFIFThumbUncompressed(BufferedImage thumb) { 879 thumbnail = thumb; 880 thumbWidth = thumb.getWidth(); 881 thumbHeight = thumb.getHeight(); 882 name = null; // not used when writing 883 } 884 readByteBuffer(ImageInputStream iis, byte [] data, JPEGImageReader reader, float workPortion, float workOffset)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 getWidth()908 int getWidth() { 909 return thumbWidth; 910 } 911 getHeight()912 int getHeight() { 913 return thumbHeight; 914 } 915 getNativeNode()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 write(ImageOutputStream ios, JPEGImageWriter writer)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 writePixels(ImageOutputStream ios, JPEGImageWriter writer)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 print()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 JFIFThumbRGB(JPEGBuffer buffer, int width, int height)965 JFIFThumbRGB(JPEGBuffer buffer, int width, int height) 966 throws IOException { 967 968 super(buffer, width, height, width*height*3, "JFIFthumbRGB"); 969 } 970 JFIFThumbRGB(Node node)971 JFIFThumbRGB(Node node) throws IIOInvalidTreeException { 972 super(node, "JFIFthumbRGB"); 973 } 974 JFIFThumbRGB(BufferedImage thumb)975 JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException { 976 super(thumb); 977 } 978 getLength()979 int getLength() { 980 return (thumbWidth*thumbHeight*3); 981 } 982 getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 write(ImageOutputStream ios, JPEGImageWriter writer)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 JFIFThumbPalette(JPEGBuffer buffer, int width, int height)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 JFIFThumbPalette(Node node)1039 JFIFThumbPalette(Node node) throws IIOInvalidTreeException { 1040 super(node, "JFIFThumbPalette"); 1041 } 1042 JFIFThumbPalette(BufferedImage thumb)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 getLength()1051 int getLength() { 1052 return (thumbWidth*thumbHeight + PALETTE_SIZE); 1053 } 1054 getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 write(ImageOutputStream ios, JPEGImageWriter writer)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 JFIFThumbJPEG(JPEGBuffer buffer, int length, JPEGImageReader reader)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 JFIFThumbJPEG(Node node)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 JFIFThumbJPEG(BufferedImage thumb)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 getWidth()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 getHeight()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; ThumbnailReadListener(JPEGImageReader reader)1247 ThumbnailReadListener (JPEGImageReader reader) { 1248 this.reader = reader; 1249 } sequenceStarted(ImageReader source, int minIndex)1250 public void sequenceStarted(ImageReader source, int minIndex) {} sequenceComplete(ImageReader source)1251 public void sequenceComplete(ImageReader source) {} imageStarted(ImageReader source, int imageIndex)1252 public void imageStarted(ImageReader source, int imageIndex) {} imageProgress(ImageReader source, float percentageDone)1253 public void imageProgress(ImageReader source, 1254 float percentageDone) { 1255 reader.thumbnailProgress(percentageDone); 1256 } imageComplete(ImageReader source)1257 public void imageComplete(ImageReader source) {} thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex)1258 public void thumbnailStarted(ImageReader source, 1259 int imageIndex, int thumbnailIndex) {} thumbnailProgress(ImageReader source, float percentageDone)1260 public void thumbnailProgress(ImageReader source, float percentageDone) {} thumbnailComplete(ImageReader source)1261 public void thumbnailComplete(ImageReader source) {} readAborted(ImageReader source)1262 public void readAborted(ImageReader source) {} 1263 } 1264 getThumbnail(ImageInputStream iis, JPEGImageReader reader)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 clone()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 getNativeNode()1288 IIOMetadataNode getNativeNode() { 1289 IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG"); 1290 if (thumbMetadata != null) { 1291 node.appendChild(thumbMetadata.getNativeTree()); 1292 } 1293 return node; 1294 } 1295 getLength()1296 int getLength() { 1297 if (data == null) { 1298 return 0; 1299 } else { 1300 return data.length; 1301 } 1302 } 1303 write(ImageOutputStream ios, JPEGImageWriter writer)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 print()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 */ writeICC(ICC_Profile profile, ImageOutputStream ios)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 ICCMarkerSegment(ICC_ColorSpace cs)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 ICCMarkerSegment(JPEGBuffer buffer)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 ICCMarkerSegment(Node node)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 clone()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 addData(JPEGBuffer buffer)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 getNativeNode()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 */ write(ImageOutputStream ios)1554 void write(ImageOutputStream ios) throws IOException { 1555 // No-op 1556 } 1557 print()1558 void print () { 1559 printTag("ICC Profile APP2"); 1560 } 1561 } 1562 } 1563