1 /* 2 * Copyright (c) 2001, 2004, 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.ImageTypeSpecifier; 29 import javax.imageio.ImageWriteParam; 30 import javax.imageio.IIOException; 31 import javax.imageio.stream.ImageInputStream; 32 import javax.imageio.stream.ImageOutputStream; 33 import javax.imageio.metadata.IIOMetadata; 34 import javax.imageio.metadata.IIOMetadataNode; 35 import javax.imageio.metadata.IIOMetadataFormat; 36 import javax.imageio.metadata.IIOMetadataFormatImpl; 37 import javax.imageio.metadata.IIOInvalidTreeException; 38 import javax.imageio.plugins.jpeg.JPEGQTable; 39 import javax.imageio.plugins.jpeg.JPEGHuffmanTable; 40 import javax.imageio.plugins.jpeg.JPEGImageWriteParam; 41 42 import org.w3c.dom.Node; 43 import org.w3c.dom.NodeList; 44 import org.w3c.dom.NamedNodeMap; 45 46 import java.util.List; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Iterator; 50 import java.util.ListIterator; 51 import java.io.IOException; 52 import java.awt.color.ICC_Profile; 53 import java.awt.color.ICC_ColorSpace; 54 import java.awt.color.ColorSpace; 55 import java.awt.image.ColorModel; 56 import java.awt.Point; 57 58 /** 59 * Metadata for the JPEG plug-in. 60 */ 61 public class JPEGMetadata extends IIOMetadata implements Cloneable { 62 63 //////// Private variables 64 65 private static final boolean debug = false; 66 67 /** 68 * A copy of <code>markerSequence</code>, created the first time the 69 * <code>markerSequence</code> is modified. This is used by reset 70 * to restore the original state. 71 */ 72 private List resetSequence = null; 73 74 /** 75 * Set to <code>true</code> when reading a thumbnail stored as 76 * JPEG. This is used to enforce the prohibition of JFIF thumbnails 77 * containing any JFIF marker segments, and to ensure generation of 78 * a correct native subtree during <code>getAsTree</code>. 79 */ 80 private boolean inThumb = false; 81 82 /** 83 * Set by the chroma node construction method to signal the 84 * presence or absence of an alpha channel to the transparency 85 * node construction method. Used only when constructing a 86 * standard metadata tree. 87 */ 88 private boolean hasAlpha; 89 90 //////// end of private variables 91 92 /////// Package-access variables 93 94 /** 95 * All data is a list of <code>MarkerSegment</code> objects. 96 * When accessing the list, use the tag to identify the particular 97 * subclass. Any JFIF marker segment must be the first element 98 * of the list if it is present, and any JFXX or APP2ICC marker 99 * segments are subordinate to the JFIF marker segment. This 100 * list is package visible so that the writer can access it. 101 * @see #MarkerSegment 102 */ 103 List markerSequence = new ArrayList(); 104 105 /** 106 * Indicates whether this object represents stream or image 107 * metadata. Package-visible so the writer can see it. 108 */ 109 final boolean isStream; 110 111 /////// End of package-access variables 112 113 /////// Constructors 114 115 /** 116 * Constructor containing code shared by other constructors. 117 */ 118 JPEGMetadata(boolean isStream, boolean inThumb) { 119 super(true, // Supports standard format 120 JPEG.nativeImageMetadataFormatName, // and a native format 121 JPEG.nativeImageMetadataFormatClassName, 122 null, null); // No other formats 123 this.inThumb = inThumb; 124 // But if we are stream metadata, adjust the variables 125 this.isStream = isStream; 126 if (isStream) { 127 nativeMetadataFormatName = JPEG.nativeStreamMetadataFormatName; 128 nativeMetadataFormatClassName = 129 JPEG.nativeStreamMetadataFormatClassName; 130 } 131 } 132 133 /* 134 * Constructs a <code>JPEGMetadata</code> object by reading the 135 * contents of an <code>ImageInputStream</code>. Has package-only 136 * access. 137 * 138 * @param isStream A boolean indicating whether this object will be 139 * stream or image metadata. 140 * @param isThumb A boolean indicating whether this metadata object 141 * is for an image or for a thumbnail stored as JPEG. 142 * @param iis An <code>ImageInputStream</code> from which to read 143 * the metadata. 144 * @param reader The <code>JPEGImageReader</code> calling this 145 * constructor, to which warnings should be sent. 146 */ 147 JPEGMetadata(boolean isStream, 148 boolean isThumb, 149 ImageInputStream iis, 150 JPEGImageReader reader) throws IOException { 151 this(isStream, isThumb); 152 153 JPEGBuffer buffer = new JPEGBuffer(iis); 154 155 buffer.loadBuf(0); 156 157 // The first three bytes should be FF, SOI, FF 158 if (((buffer.buf[0] & 0xff) != 0xff) 159 || ((buffer.buf[1] & 0xff) != JPEG.SOI) 160 || ((buffer.buf[2] & 0xff) != 0xff)) { 161 throw new IIOException ("Image format error"); 162 } 163 164 boolean done = false; 165 buffer.bufAvail -=2; // Next byte should be the ff before a marker 166 buffer.bufPtr = 2; 167 MarkerSegment newGuy = null; 168 while (!done) { 169 byte [] buf; 170 int ptr; 171 buffer.loadBuf(1); 172 if (debug) { 173 System.out.println("top of loop"); 174 buffer.print(10); 175 } 176 buffer.scanForFF(reader); 177 switch (buffer.buf[buffer.bufPtr] & 0xff) { 178 case 0: 179 if (debug) { 180 System.out.println("Skipping 0"); 181 } 182 buffer.bufAvail--; 183 buffer.bufPtr++; 184 break; 185 case JPEG.SOF0: 186 case JPEG.SOF1: 187 case JPEG.SOF2: 188 if (isStream) { 189 throw new IIOException 190 ("SOF not permitted in stream metadata"); 191 } 192 newGuy = new SOFMarkerSegment(buffer); 193 break; 194 case JPEG.DQT: 195 newGuy = new DQTMarkerSegment(buffer); 196 break; 197 case JPEG.DHT: 198 newGuy = new DHTMarkerSegment(buffer); 199 break; 200 case JPEG.DRI: 201 newGuy = new DRIMarkerSegment(buffer); 202 break; 203 case JPEG.APP0: 204 // Either JFIF, JFXX, or unknown APP0 205 buffer.loadBuf(8); // tag, length, id 206 buf = buffer.buf; 207 ptr = buffer.bufPtr; 208 if ((buf[ptr+3] == 'J') 209 && (buf[ptr+4] == 'F') 210 && (buf[ptr+5] == 'I') 211 && (buf[ptr+6] == 'F') 212 && (buf[ptr+7] == 0)) { 213 if (inThumb) { 214 reader.warningOccurred 215 (JPEGImageReader.WARNING_NO_JFIF_IN_THUMB); 216 // Leave newGuy null 217 // Read a dummy to skip the segment 218 JFIFMarkerSegment dummy = 219 new JFIFMarkerSegment(buffer); 220 } else if (isStream) { 221 throw new IIOException 222 ("JFIF not permitted in stream metadata"); 223 } else if (markerSequence.isEmpty() == false) { 224 throw new IIOException 225 ("JFIF APP0 must be first marker after SOI"); 226 } else { 227 newGuy = new JFIFMarkerSegment(buffer); 228 } 229 } else if ((buf[ptr+3] == 'J') 230 && (buf[ptr+4] == 'F') 231 && (buf[ptr+5] == 'X') 232 && (buf[ptr+6] == 'X') 233 && (buf[ptr+7] == 0)) { 234 if (isStream) { 235 throw new IIOException 236 ("JFXX not permitted in stream metadata"); 237 } 238 if (inThumb) { 239 throw new IIOException 240 ("JFXX markers not allowed in JFIF JPEG thumbnail"); 241 } 242 JFIFMarkerSegment jfif = 243 (JFIFMarkerSegment) findMarkerSegment 244 (JFIFMarkerSegment.class, true); 245 if (jfif == null) { 246 throw new IIOException 247 ("JFXX encountered without prior JFIF!"); 248 } 249 jfif.addJFXX(buffer, reader); 250 // newGuy remains null 251 } else { 252 newGuy = new MarkerSegment(buffer); 253 newGuy.loadData(buffer); 254 } 255 break; 256 case JPEG.APP2: 257 // Either an ICC profile or unknown APP2 258 buffer.loadBuf(15); // tag, length, id 259 if ((buffer.buf[buffer.bufPtr+3] == 'I') 260 && (buffer.buf[buffer.bufPtr+4] == 'C') 261 && (buffer.buf[buffer.bufPtr+5] == 'C') 262 && (buffer.buf[buffer.bufPtr+6] == '_') 263 && (buffer.buf[buffer.bufPtr+7] == 'P') 264 && (buffer.buf[buffer.bufPtr+8] == 'R') 265 && (buffer.buf[buffer.bufPtr+9] == 'O') 266 && (buffer.buf[buffer.bufPtr+10] == 'F') 267 && (buffer.buf[buffer.bufPtr+11] == 'I') 268 && (buffer.buf[buffer.bufPtr+12] == 'L') 269 && (buffer.buf[buffer.bufPtr+13] == 'E') 270 && (buffer.buf[buffer.bufPtr+14] == 0) 271 ) { 272 if (isStream) { 273 throw new IIOException 274 ("ICC profiles not permitted in stream metadata"); 275 } 276 277 JFIFMarkerSegment jfif = 278 (JFIFMarkerSegment) findMarkerSegment 279 (JFIFMarkerSegment.class, true); 280 if (jfif == null) { 281 throw new IIOException 282 ("ICC APP2 encountered without prior JFIF!"); 283 } 284 jfif.addICC(buffer); 285 // newGuy remains null 286 } else { 287 newGuy = new MarkerSegment(buffer); 288 newGuy.loadData(buffer); 289 } 290 break; 291 case JPEG.APP14: 292 // Either Adobe or unknown APP14 293 buffer.loadBuf(8); // tag, length, id 294 if ((buffer.buf[buffer.bufPtr+3] == 'A') 295 && (buffer.buf[buffer.bufPtr+4] == 'd') 296 && (buffer.buf[buffer.bufPtr+5] == 'o') 297 && (buffer.buf[buffer.bufPtr+6] == 'b') 298 && (buffer.buf[buffer.bufPtr+7] == 'e')) { 299 if (isStream) { 300 throw new IIOException 301 ("Adobe APP14 markers not permitted in stream metadata"); 302 } 303 newGuy = new AdobeMarkerSegment(buffer); 304 } else { 305 newGuy = new MarkerSegment(buffer); 306 newGuy.loadData(buffer); 307 } 308 309 break; 310 case JPEG.COM: 311 newGuy = new COMMarkerSegment(buffer); 312 break; 313 case JPEG.SOS: 314 if (isStream) { 315 throw new IIOException 316 ("SOS not permitted in stream metadata"); 317 } 318 newGuy = new SOSMarkerSegment(buffer); 319 break; 320 case JPEG.RST0: 321 case JPEG.RST1: 322 case JPEG.RST2: 323 case JPEG.RST3: 324 case JPEG.RST4: 325 case JPEG.RST5: 326 case JPEG.RST6: 327 case JPEG.RST7: 328 if (debug) { 329 System.out.println("Restart Marker"); 330 } 331 buffer.bufPtr++; // Just skip it 332 buffer.bufAvail--; 333 break; 334 case JPEG.EOI: 335 done = true; 336 buffer.bufPtr++; 337 buffer.bufAvail--; 338 break; 339 default: 340 newGuy = new MarkerSegment(buffer); 341 newGuy.loadData(buffer); 342 newGuy.unknown = true; 343 break; 344 } 345 if (newGuy != null) { 346 markerSequence.add(newGuy); 347 if (debug) { 348 newGuy.print(); 349 } 350 newGuy = null; 351 } 352 } 353 354 // Now that we've read up to the EOI, we need to push back 355 // whatever is left in the buffer, so that the next read 356 // in the native code will work. 357 358 buffer.pushBack(); 359 360 if (!isConsistent()) { 361 throw new IIOException("Inconsistent metadata read from stream"); 362 } 363 } 364 365 /** 366 * Constructs a default stream <code>JPEGMetadata</code> object appropriate 367 * for the given write parameters. 368 */ 369 JPEGMetadata(ImageWriteParam param, JPEGImageWriter writer) { 370 this(true, false); 371 372 JPEGImageWriteParam jparam = null; 373 374 if ((param != null) && (param instanceof JPEGImageWriteParam)) { 375 jparam = (JPEGImageWriteParam) param; 376 if (!jparam.areTablesSet()) { 377 jparam = null; 378 } 379 } 380 if (jparam != null) { 381 markerSequence.add(new DQTMarkerSegment(jparam.getQTables())); 382 markerSequence.add 383 (new DHTMarkerSegment(jparam.getDCHuffmanTables(), 384 jparam.getACHuffmanTables())); 385 } else { 386 // default tables. 387 markerSequence.add(new DQTMarkerSegment(JPEG.getDefaultQTables())); 388 markerSequence.add(new DHTMarkerSegment(JPEG.getDefaultHuffmanTables(true), 389 JPEG.getDefaultHuffmanTables(false))); 390 } 391 392 // Defensive programming 393 if (!isConsistent()) { 394 throw new InternalError("Default stream metadata is inconsistent"); 395 } 396 } 397 398 /** 399 * Constructs a default image <code>JPEGMetadata</code> object appropriate 400 * for the given image type and write parameters. 401 */ 402 JPEGMetadata(ImageTypeSpecifier imageType, 403 ImageWriteParam param, 404 JPEGImageWriter writer) { 405 this(false, false); 406 407 boolean wantJFIF = true; 408 boolean wantAdobe = false; 409 int transform = JPEG.ADOBE_UNKNOWN; 410 boolean willSubsample = true; 411 boolean wantICC = false; 412 boolean wantProg = false; 413 boolean wantOptimized = false; 414 boolean wantExtended = false; 415 boolean wantQTables = true; 416 boolean wantHTables = true; 417 float quality = JPEG.DEFAULT_QUALITY; 418 byte[] componentIDs = { 1, 2, 3, 4}; 419 int numComponents = 0; 420 421 ImageTypeSpecifier destType = null; 422 423 if (param != null) { 424 destType = param.getDestinationType(); 425 if (destType != null) { 426 if (imageType != null) { 427 // Ignore the destination type. 428 writer.warningOccurred 429 (JPEGImageWriter.WARNING_DEST_IGNORED); 430 destType = null; 431 } 432 } 433 // The only progressive mode that makes sense here is MODE_DEFAULT 434 if (param.canWriteProgressive()) { 435 // the param may not be one of ours, so it may return false. 436 // If so, the following would throw an exception 437 if (param.getProgressiveMode() == ImageWriteParam.MODE_DEFAULT) { 438 wantProg = true; 439 wantOptimized = true; 440 wantHTables = false; 441 } 442 } 443 444 if (param instanceof JPEGImageWriteParam) { 445 JPEGImageWriteParam jparam = (JPEGImageWriteParam) param; 446 if (jparam.areTablesSet()) { 447 wantQTables = false; // If the param has them, metadata shouldn't 448 wantHTables = false; 449 if ((jparam.getDCHuffmanTables().length > 2) 450 || (jparam.getACHuffmanTables().length > 2)) { 451 wantExtended = true; 452 } 453 } 454 // Progressive forces optimized, regardless of param setting 455 // so consult the param re optimized only if not progressive 456 if (!wantProg) { 457 wantOptimized = jparam.getOptimizeHuffmanTables(); 458 if (wantOptimized) { 459 wantHTables = false; 460 } 461 } 462 } 463 464 // compression quality should determine the q tables. Note that this 465 // will be ignored if we already decided not to create any. 466 // Again, the param may not be one of ours, so we must check that it 467 // supports compression settings 468 if (param.canWriteCompressed()) { 469 if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { 470 quality = param.getCompressionQuality(); 471 } 472 } 473 } 474 475 // We are done with the param, now for the image types 476 477 ColorSpace cs = null; 478 if (destType != null) { 479 ColorModel cm = destType.getColorModel(); 480 numComponents = cm.getNumComponents(); 481 boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents); 482 boolean hasAlpha = cm.hasAlpha(); 483 cs = cm.getColorSpace(); 484 int type = cs.getType(); 485 switch(type) { 486 case ColorSpace.TYPE_GRAY: 487 willSubsample = false; 488 if (hasExtraComponents) { // e.g. alpha 489 wantJFIF = false; 490 } 491 break; 492 case ColorSpace.TYPE_3CLR: 493 if (cs == JPEG.JCS.getYCC()) { 494 wantJFIF = false; 495 componentIDs[0] = (byte) 'Y'; 496 componentIDs[1] = (byte) 'C'; 497 componentIDs[2] = (byte) 'c'; 498 if (hasAlpha) { 499 componentIDs[3] = (byte) 'A'; 500 } 501 } 502 break; 503 case ColorSpace.TYPE_YCbCr: 504 if (hasExtraComponents) { // e.g. K or alpha 505 wantJFIF = false; 506 if (!hasAlpha) { // Not alpha, so must be K 507 wantAdobe = true; 508 transform = JPEG.ADOBE_YCCK; 509 } 510 } 511 break; 512 case ColorSpace.TYPE_RGB: // with or without alpha 513 wantJFIF = false; 514 wantAdobe = true; 515 willSubsample = false; 516 componentIDs[0] = (byte) 'R'; 517 componentIDs[1] = (byte) 'G'; 518 componentIDs[2] = (byte) 'B'; 519 if (hasAlpha) { 520 componentIDs[3] = (byte) 'A'; 521 } 522 break; 523 default: 524 // Everything else is not subsampled, gets no special marker, 525 // and component ids are 1 - N 526 wantJFIF = false; 527 willSubsample = false; 528 } 529 } else if (imageType != null) { 530 ColorModel cm = imageType.getColorModel(); 531 numComponents = cm.getNumComponents(); 532 boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents); 533 boolean hasAlpha = cm.hasAlpha(); 534 cs = cm.getColorSpace(); 535 int type = cs.getType(); 536 switch(type) { 537 case ColorSpace.TYPE_GRAY: 538 willSubsample = false; 539 if (hasExtraComponents) { // e.g. alpha 540 wantJFIF = false; 541 } 542 break; 543 case ColorSpace.TYPE_RGB: // with or without alpha 544 // without alpha we just accept the JFIF defaults 545 if (hasAlpha) { 546 wantJFIF = false; 547 } 548 break; 549 case ColorSpace.TYPE_3CLR: 550 wantJFIF = false; 551 willSubsample = false; 552 if (cs.equals(ColorSpace.getInstance(ColorSpace.CS_PYCC))) { 553 willSubsample = true; 554 wantAdobe = true; 555 componentIDs[0] = (byte) 'Y'; 556 componentIDs[1] = (byte) 'C'; 557 componentIDs[2] = (byte) 'c'; 558 if (hasAlpha) { 559 componentIDs[3] = (byte) 'A'; 560 } 561 } 562 break; 563 case ColorSpace.TYPE_YCbCr: 564 if (hasExtraComponents) { // e.g. K or alpha 565 wantJFIF = false; 566 if (!hasAlpha) { // then it must be K 567 wantAdobe = true; 568 transform = JPEG.ADOBE_YCCK; 569 } 570 } 571 break; 572 case ColorSpace.TYPE_CMYK: 573 wantJFIF = false; 574 wantAdobe = true; 575 transform = JPEG.ADOBE_YCCK; 576 break; 577 578 default: 579 // Everything else is not subsampled, gets no special marker, 580 // and component ids are 0 - N 581 wantJFIF = false; 582 willSubsample = false; 583 } 584 585 } 586 587 // do we want an ICC profile? 588 if (wantJFIF && JPEG.isNonStandardICC(cs)) { 589 wantICC = true; 590 } 591 592 // Now step through the markers, consulting our variables. 593 if (wantJFIF) { 594 JFIFMarkerSegment jfif = new JFIFMarkerSegment(); 595 markerSequence.add(jfif); 596 if (wantICC) { 597 try { 598 jfif.addICC((ICC_ColorSpace)cs); 599 } catch (IOException e) {} // Can't happen here 600 } 601 } 602 // Adobe 603 if (wantAdobe) { 604 markerSequence.add(new AdobeMarkerSegment(transform)); 605 } 606 607 // dqt 608 if (wantQTables) { 609 markerSequence.add(new DQTMarkerSegment(quality, willSubsample)); 610 } 611 612 // dht 613 if (wantHTables) { 614 markerSequence.add(new DHTMarkerSegment(willSubsample)); 615 } 616 617 // sof 618 markerSequence.add(new SOFMarkerSegment(wantProg, 619 wantExtended, 620 willSubsample, 621 componentIDs, 622 numComponents)); 623 624 // sos 625 if (!wantProg) { // Default progression scans are done in the writer 626 markerSequence.add(new SOSMarkerSegment(willSubsample, 627 componentIDs, 628 numComponents)); 629 } 630 631 // Defensive programming 632 if (!isConsistent()) { 633 throw new InternalError("Default image metadata is inconsistent"); 634 } 635 } 636 637 ////// End of constructors 638 639 // Utilities for dealing with the marker sequence. 640 // The first ones have package access for access from the writer. 641 642 /** 643 * Returns the first MarkerSegment object in the list 644 * with the given tag, or null if none is found. 645 */ 646 MarkerSegment findMarkerSegment(int tag) { 647 Iterator iter = markerSequence.iterator(); 648 while (iter.hasNext()) { 649 MarkerSegment seg = (MarkerSegment)iter.next(); 650 if (seg.tag == tag) { 651 return seg; 652 } 653 } 654 return null; 655 } 656 657 /** 658 * Returns the first or last MarkerSegment object in the list 659 * of the given class, or null if none is found. 660 */ 661 MarkerSegment findMarkerSegment(Class cls, boolean first) { 662 if (first) { 663 Iterator iter = markerSequence.iterator(); 664 while (iter.hasNext()) { 665 MarkerSegment seg = (MarkerSegment)iter.next(); 666 if (cls.isInstance(seg)) { 667 return seg; 668 } 669 } 670 } else { 671 ListIterator iter = markerSequence.listIterator(markerSequence.size()); 672 while (iter.hasPrevious()) { 673 MarkerSegment seg = (MarkerSegment)iter.previous(); 674 if (cls.isInstance(seg)) { 675 return seg; 676 } 677 } 678 } 679 return null; 680 } 681 682 /** 683 * Returns the index of the first or last MarkerSegment in the list 684 * of the given class, or -1 if none is found. 685 */ 686 private int findMarkerSegmentPosition(Class cls, boolean first) { 687 if (first) { 688 ListIterator iter = markerSequence.listIterator(); 689 for (int i = 0; iter.hasNext(); i++) { 690 MarkerSegment seg = (MarkerSegment)iter.next(); 691 if (cls.isInstance(seg)) { 692 return i; 693 } 694 } 695 } else { 696 ListIterator iter = markerSequence.listIterator(markerSequence.size()); 697 for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) { 698 MarkerSegment seg = (MarkerSegment)iter.previous(); 699 if (cls.isInstance(seg)) { 700 return i; 701 } 702 } 703 } 704 return -1; 705 } 706 707 private int findLastUnknownMarkerSegmentPosition() { 708 ListIterator iter = markerSequence.listIterator(markerSequence.size()); 709 for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) { 710 MarkerSegment seg = (MarkerSegment)iter.previous(); 711 if (seg.unknown == true) { 712 return i; 713 } 714 } 715 return -1; 716 } 717 718 // Implement Cloneable, but restrict access 719 720 protected Object clone() { 721 JPEGMetadata newGuy = null; 722 try { 723 newGuy = (JPEGMetadata) super.clone(); 724 } catch (CloneNotSupportedException e) {} // won't happen 725 if (markerSequence != null) { 726 newGuy.markerSequence = cloneSequence(); 727 } 728 newGuy.resetSequence = null; 729 return newGuy; 730 } 731 732 /** 733 * Returns a deep copy of the current marker sequence. 734 */ 735 private List cloneSequence() { 736 if (markerSequence == null) { 737 return null; 738 } 739 List retval = new ArrayList(markerSequence.size()); 740 Iterator iter = markerSequence.iterator(); 741 while(iter.hasNext()) { 742 MarkerSegment seg = (MarkerSegment)iter.next(); 743 retval.add(seg.clone()); 744 } 745 746 return retval; 747 } 748 749 750 // Tree methods 751 752 public Node getAsTree(String formatName) { 753 if (formatName == null) { 754 throw new IllegalArgumentException("null formatName!"); 755 } 756 if (isStream) { 757 if (formatName.equals(JPEG.nativeStreamMetadataFormatName)) { 758 return getNativeTree(); 759 } 760 } else { 761 if (formatName.equals(JPEG.nativeImageMetadataFormatName)) { 762 return getNativeTree(); 763 } 764 if (formatName.equals 765 (IIOMetadataFormatImpl.standardMetadataFormatName)) { 766 return getStandardTree(); 767 } 768 } 769 throw new IllegalArgumentException("Unsupported format name: " 770 + formatName); 771 } 772 773 IIOMetadataNode getNativeTree() { 774 IIOMetadataNode root; 775 IIOMetadataNode top; 776 Iterator iter = markerSequence.iterator(); 777 if (isStream) { 778 root = new IIOMetadataNode(JPEG.nativeStreamMetadataFormatName); 779 top = root; 780 } else { 781 IIOMetadataNode sequence = new IIOMetadataNode("markerSequence"); 782 if (!inThumb) { 783 root = new IIOMetadataNode(JPEG.nativeImageMetadataFormatName); 784 IIOMetadataNode header = new IIOMetadataNode("JPEGvariety"); 785 root.appendChild(header); 786 JFIFMarkerSegment jfif = (JFIFMarkerSegment) 787 findMarkerSegment(JFIFMarkerSegment.class, true); 788 if (jfif != null) { 789 iter.next(); // JFIF must be first, so this skips it 790 header.appendChild(jfif.getNativeNode()); 791 } 792 root.appendChild(sequence); 793 } else { 794 root = sequence; 795 } 796 top = sequence; 797 } 798 while(iter.hasNext()) { 799 MarkerSegment seg = (MarkerSegment) iter.next(); 800 top.appendChild(seg.getNativeNode()); 801 } 802 return root; 803 } 804 805 // Standard tree node methods 806 807 protected IIOMetadataNode getStandardChromaNode() { 808 hasAlpha = false; // Unless we find otherwise 809 810 // Colorspace type - follow the rules in the spec 811 // First get the SOF marker segment, if there is one 812 SOFMarkerSegment sof = (SOFMarkerSegment) 813 findMarkerSegment(SOFMarkerSegment.class, true); 814 if (sof == null) { 815 // No image, so no chroma 816 return null; 817 } 818 819 IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); 820 IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); 821 chroma.appendChild(csType); 822 823 // get the number of channels 824 int numChannels = sof.componentSpecs.length; 825 826 IIOMetadataNode numChanNode = new IIOMetadataNode("NumChannels"); 827 chroma.appendChild(numChanNode); 828 numChanNode.setAttribute("value", Integer.toString(numChannels)); 829 830 // is there a JFIF marker segment? 831 if (findMarkerSegment(JFIFMarkerSegment.class, true) != null) { 832 if (numChannels == 1) { 833 csType.setAttribute("name", "GRAY"); 834 } else { 835 csType.setAttribute("name", "YCbCr"); 836 } 837 return chroma; 838 } 839 840 // How about an Adobe marker segment? 841 AdobeMarkerSegment adobe = 842 (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); 843 if (adobe != null){ 844 switch (adobe.transform) { 845 case JPEG.ADOBE_YCCK: 846 csType.setAttribute("name", "YCCK"); 847 break; 848 case JPEG.ADOBE_YCC: 849 csType.setAttribute("name", "YCbCr"); 850 break; 851 case JPEG.ADOBE_UNKNOWN: 852 if (numChannels == 3) { 853 csType.setAttribute("name", "RGB"); 854 } else if (numChannels == 4) { 855 csType.setAttribute("name", "CMYK"); 856 } 857 break; 858 } 859 return chroma; 860 } 861 862 // Neither marker. Check components 863 if (numChannels < 3) { 864 csType.setAttribute("name", "GRAY"); 865 if (numChannels == 2) { 866 hasAlpha = true; 867 } 868 return chroma; 869 } 870 871 boolean idsAreJFIF = true; 872 873 for (int i = 0; i < sof.componentSpecs.length; i++) { 874 int id = sof.componentSpecs[i].componentId; 875 if ((id < 1) || (id >= sof.componentSpecs.length)) { 876 idsAreJFIF = false; 877 } 878 } 879 880 if (idsAreJFIF) { 881 csType.setAttribute("name", "YCbCr"); 882 if (numChannels == 4) { 883 hasAlpha = true; 884 } 885 return chroma; 886 } 887 888 // Check against the letters 889 if ((sof.componentSpecs[0].componentId == 'R') 890 && (sof.componentSpecs[1].componentId == 'G') 891 && (sof.componentSpecs[2].componentId == 'B')){ 892 893 csType.setAttribute("name", "RGB"); 894 if ((numChannels == 4) 895 && (sof.componentSpecs[3].componentId == 'A')) { 896 hasAlpha = true; 897 } 898 return chroma; 899 } 900 901 if ((sof.componentSpecs[0].componentId == 'Y') 902 && (sof.componentSpecs[1].componentId == 'C') 903 && (sof.componentSpecs[2].componentId == 'c')){ 904 905 csType.setAttribute("name", "PhotoYCC"); 906 if ((numChannels == 4) 907 && (sof.componentSpecs[3].componentId == 'A')) { 908 hasAlpha = true; 909 } 910 return chroma; 911 } 912 913 // Finally, 3-channel subsampled are YCbCr, unsubsampled are RGB 914 // 4-channel subsampled are YCbCrA, unsubsampled are CMYK 915 916 boolean subsampled = false; 917 918 int hfactor = sof.componentSpecs[0].HsamplingFactor; 919 int vfactor = sof.componentSpecs[0].VsamplingFactor; 920 921 for (int i = 1; i<sof.componentSpecs.length; i++) { 922 if ((sof.componentSpecs[i].HsamplingFactor != hfactor) 923 || (sof.componentSpecs[i].VsamplingFactor != vfactor)){ 924 subsampled = true; 925 break; 926 } 927 } 928 929 if (subsampled) { 930 csType.setAttribute("name", "YCbCr"); 931 if (numChannels == 4) { 932 hasAlpha = true; 933 } 934 return chroma; 935 } 936 937 // Not subsampled. numChannels < 3 is taken care of above 938 if (numChannels == 3) { 939 csType.setAttribute("name", "RGB"); 940 } else { 941 csType.setAttribute("name", "CMYK"); 942 } 943 944 return chroma; 945 } 946 947 protected IIOMetadataNode getStandardCompressionNode() { 948 949 IIOMetadataNode compression = new IIOMetadataNode("Compression"); 950 951 // CompressionTypeName 952 IIOMetadataNode name = new IIOMetadataNode("CompressionTypeName"); 953 name.setAttribute("value", "JPEG"); 954 compression.appendChild(name); 955 956 // Lossless - false 957 IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); 958 lossless.setAttribute("value", "FALSE"); 959 compression.appendChild(lossless); 960 961 // NumProgressiveScans - count sos segments 962 int sosCount = 0; 963 Iterator iter = markerSequence.iterator(); 964 while (iter.hasNext()) { 965 MarkerSegment ms = (MarkerSegment) iter.next(); 966 if (ms.tag == JPEG.SOS) { 967 sosCount++; 968 } 969 } 970 if (sosCount != 0) { 971 IIOMetadataNode prog = new IIOMetadataNode("NumProgressiveScans"); 972 prog.setAttribute("value", Integer.toString(sosCount)); 973 compression.appendChild(prog); 974 } 975 976 return compression; 977 } 978 979 protected IIOMetadataNode getStandardDimensionNode() { 980 // If we have a JFIF marker segment, we know a little 981 // otherwise all we know is the orientation, which is always normal 982 IIOMetadataNode dim = new IIOMetadataNode("Dimension"); 983 IIOMetadataNode orient = new IIOMetadataNode("ImageOrientation"); 984 orient.setAttribute("value", "normal"); 985 dim.appendChild(orient); 986 987 JFIFMarkerSegment jfif = 988 (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); 989 if (jfif != null) { 990 991 // Aspect Ratio is width of pixel / height of pixel 992 float aspectRatio; 993 if (jfif.resUnits == 0) { 994 // In this case they just encode aspect ratio directly 995 aspectRatio = ((float) jfif.Xdensity)/jfif.Ydensity; 996 } else { 997 // They are true densities (e.g. dpi) and must be inverted 998 aspectRatio = ((float) jfif.Ydensity)/jfif.Xdensity; 999 } 1000 IIOMetadataNode aspect = new IIOMetadataNode("PixelAspectRatio"); 1001 aspect.setAttribute("value", Float.toString(aspectRatio)); 1002 dim.insertBefore(aspect, orient); 1003 1004 // Pixel size 1005 if (jfif.resUnits != 0) { 1006 // 1 == dpi, 2 == dpc 1007 float scale = (jfif.resUnits == 1) ? 25.4F : 10.0F; 1008 1009 IIOMetadataNode horiz = 1010 new IIOMetadataNode("HorizontalPixelSize"); 1011 horiz.setAttribute("value", 1012 Float.toString(scale/jfif.Xdensity)); 1013 dim.appendChild(horiz); 1014 1015 IIOMetadataNode vert = 1016 new IIOMetadataNode("VerticalPixelSize"); 1017 vert.setAttribute("value", 1018 Float.toString(scale/jfif.Ydensity)); 1019 dim.appendChild(vert); 1020 } 1021 } 1022 return dim; 1023 } 1024 1025 protected IIOMetadataNode getStandardTextNode() { 1026 IIOMetadataNode text = null; 1027 // Add a text entry for each COM Marker Segment 1028 if (findMarkerSegment(JPEG.COM) != null) { 1029 text = new IIOMetadataNode("Text"); 1030 Iterator iter = markerSequence.iterator(); 1031 while (iter.hasNext()) { 1032 MarkerSegment seg = (MarkerSegment) iter.next(); 1033 if (seg.tag == JPEG.COM) { 1034 COMMarkerSegment com = (COMMarkerSegment) seg; 1035 IIOMetadataNode entry = new IIOMetadataNode("TextEntry"); 1036 entry.setAttribute("keyword", "comment"); 1037 entry.setAttribute("value", com.getComment()); 1038 text.appendChild(entry); 1039 } 1040 } 1041 } 1042 return text; 1043 } 1044 1045 protected IIOMetadataNode getStandardTransparencyNode() { 1046 IIOMetadataNode trans = null; 1047 if (hasAlpha == true) { 1048 trans = new IIOMetadataNode("Transparency"); 1049 IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); 1050 alpha.setAttribute("value", "nonpremultiplied"); // Always assume 1051 trans.appendChild(alpha); 1052 } 1053 return trans; 1054 } 1055 1056 // Editing 1057 1058 public boolean isReadOnly() { 1059 return false; 1060 } 1061 1062 public void mergeTree(String formatName, Node root) 1063 throws IIOInvalidTreeException { 1064 if (formatName == null) { 1065 throw new IllegalArgumentException("null formatName!"); 1066 } 1067 if (root == null) { 1068 throw new IllegalArgumentException("null root!"); 1069 } 1070 List copy = null; 1071 if (resetSequence == null) { 1072 resetSequence = cloneSequence(); // Deep copy 1073 copy = resetSequence; // Avoid cloning twice 1074 } else { 1075 copy = cloneSequence(); 1076 } 1077 if (isStream && 1078 (formatName.equals(JPEG.nativeStreamMetadataFormatName))) { 1079 mergeNativeTree(root); 1080 } else if (!isStream && 1081 (formatName.equals(JPEG.nativeImageMetadataFormatName))) { 1082 mergeNativeTree(root); 1083 } else if (!isStream && 1084 (formatName.equals 1085 (IIOMetadataFormatImpl.standardMetadataFormatName))) { 1086 mergeStandardTree(root); 1087 } else { 1088 throw new IllegalArgumentException("Unsupported format name: " 1089 + formatName); 1090 } 1091 if (!isConsistent()) { 1092 markerSequence = copy; 1093 throw new IIOInvalidTreeException 1094 ("Merged tree is invalid; original restored", root); 1095 } 1096 } 1097 1098 private void mergeNativeTree(Node root) throws IIOInvalidTreeException { 1099 String name = root.getNodeName(); 1100 if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName 1101 : JPEG.nativeImageMetadataFormatName)) { 1102 throw new IIOInvalidTreeException("Invalid root node name: " + name, 1103 root); 1104 } 1105 if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence 1106 throw new IIOInvalidTreeException( 1107 "JPEGvariety and markerSequence nodes must be present", root); 1108 } 1109 mergeJFIFsubtree(root.getFirstChild()); 1110 mergeSequenceSubtree(root.getLastChild()); 1111 } 1112 1113 /** 1114 * Merge a JFIF subtree into the marker sequence, if the subtree 1115 * is non-empty. 1116 * If a JFIF marker exists, update it from the subtree. 1117 * If none exists, create one from the subtree and insert it at the 1118 * beginning of the marker sequence. 1119 */ 1120 private void mergeJFIFsubtree(Node JPEGvariety) 1121 throws IIOInvalidTreeException { 1122 if (JPEGvariety.getChildNodes().getLength() != 0) { 1123 Node jfifNode = JPEGvariety.getFirstChild(); 1124 // is there already a jfif marker segment? 1125 JFIFMarkerSegment jfifSeg = 1126 (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); 1127 if (jfifSeg != null) { 1128 jfifSeg.updateFromNativeNode(jfifNode, false); 1129 } else { 1130 // Add it as the first element in the list. 1131 markerSequence.add(0, new JFIFMarkerSegment(jfifNode)); 1132 } 1133 } 1134 } 1135 1136 private void mergeSequenceSubtree(Node sequenceTree) 1137 throws IIOInvalidTreeException { 1138 NodeList children = sequenceTree.getChildNodes(); 1139 for (int i = 0; i < children.getLength(); i++) { 1140 Node node = children.item(i); 1141 String name = node.getNodeName(); 1142 if (name.equals("dqt")) { 1143 mergeDQTNode(node); 1144 } else if (name.equals("dht")) { 1145 mergeDHTNode(node); 1146 } else if (name.equals("dri")) { 1147 mergeDRINode(node); 1148 } else if (name.equals("com")) { 1149 mergeCOMNode(node); 1150 } else if (name.equals("app14Adobe")) { 1151 mergeAdobeNode(node); 1152 } else if (name.equals("unknown")) { 1153 mergeUnknownNode(node); 1154 } else if (name.equals("sof")) { 1155 mergeSOFNode(node); 1156 } else if (name.equals("sos")) { 1157 mergeSOSNode(node); 1158 } else { 1159 throw new IIOInvalidTreeException("Invalid node: " + name, node); 1160 } 1161 } 1162 } 1163 1164 /** 1165 * Merge the given DQT node into the marker sequence. If there already 1166 * exist DQT marker segments in the sequence, then each table in the 1167 * node replaces the first table, in any DQT segment, with the same 1168 * table id. If none of the existing DQT segments contain a table with 1169 * the same id, then the table is added to the last existing DQT segment. 1170 * If there are no DQT segments, then a new one is created and added 1171 * as follows: 1172 * If there are DHT segments, the new DQT segment is inserted before the 1173 * first one. 1174 * If there are no DHT segments, the new DQT segment is inserted before 1175 * an SOF segment, if there is one. 1176 * If there is no SOF segment, the new DQT segment is inserted before 1177 * the first SOS segment, if there is one. 1178 * If there is no SOS segment, the new DQT segment is added to the end 1179 * of the sequence. 1180 */ 1181 private void mergeDQTNode(Node node) throws IIOInvalidTreeException { 1182 // First collect any existing DQT nodes into a local list 1183 ArrayList oldDQTs = new ArrayList(); 1184 Iterator iter = markerSequence.iterator(); 1185 while (iter.hasNext()) { 1186 MarkerSegment seg = (MarkerSegment) iter.next(); 1187 if (seg instanceof DQTMarkerSegment) { 1188 oldDQTs.add(seg); 1189 } 1190 } 1191 if (!oldDQTs.isEmpty()) { 1192 NodeList children = node.getChildNodes(); 1193 for (int i = 0; i < children.getLength(); i++) { 1194 Node child = children.item(i); 1195 int childID = MarkerSegment.getAttributeValue(child, 1196 null, 1197 "qtableId", 1198 0, 3, 1199 true); 1200 DQTMarkerSegment dqt = null; 1201 int tableIndex = -1; 1202 for (int j = 0; j < oldDQTs.size(); j++) { 1203 DQTMarkerSegment testDQT = (DQTMarkerSegment) oldDQTs.get(j); 1204 for (int k = 0; k < testDQT.tables.size(); k++) { 1205 DQTMarkerSegment.Qtable testTable = 1206 (DQTMarkerSegment.Qtable) testDQT.tables.get(k); 1207 if (childID == testTable.tableID) { 1208 dqt = testDQT; 1209 tableIndex = k; 1210 break; 1211 } 1212 } 1213 if (dqt != null) break; 1214 } 1215 if (dqt != null) { 1216 dqt.tables.set(tableIndex, dqt.getQtableFromNode(child)); 1217 } else { 1218 dqt = (DQTMarkerSegment) oldDQTs.get(oldDQTs.size()-1); 1219 dqt.tables.add(dqt.getQtableFromNode(child)); 1220 } 1221 } 1222 } else { 1223 DQTMarkerSegment newGuy = new DQTMarkerSegment(node); 1224 int firstDHT = findMarkerSegmentPosition(DHTMarkerSegment.class, true); 1225 int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); 1226 int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); 1227 if (firstDHT != -1) { 1228 markerSequence.add(firstDHT, newGuy); 1229 } else if (firstSOF != -1) { 1230 markerSequence.add(firstSOF, newGuy); 1231 } else if (firstSOS != -1) { 1232 markerSequence.add(firstSOS, newGuy); 1233 } else { 1234 markerSequence.add(newGuy); 1235 } 1236 } 1237 } 1238 1239 /** 1240 * Merge the given DHT node into the marker sequence. If there already 1241 * exist DHT marker segments in the sequence, then each table in the 1242 * node replaces the first table, in any DHT segment, with the same 1243 * table class and table id. If none of the existing DHT segments contain 1244 * a table with the same class and id, then the table is added to the last 1245 * existing DHT segment. 1246 * If there are no DHT segments, then a new one is created and added 1247 * as follows: 1248 * If there are DQT segments, the new DHT segment is inserted immediately 1249 * following the last DQT segment. 1250 * If there are no DQT segments, the new DHT segment is inserted before 1251 * an SOF segment, if there is one. 1252 * If there is no SOF segment, the new DHT segment is inserted before 1253 * the first SOS segment, if there is one. 1254 * If there is no SOS segment, the new DHT segment is added to the end 1255 * of the sequence. 1256 */ 1257 private void mergeDHTNode(Node node) throws IIOInvalidTreeException { 1258 // First collect any existing DQT nodes into a local list 1259 ArrayList oldDHTs = new ArrayList(); 1260 Iterator iter = markerSequence.iterator(); 1261 while (iter.hasNext()) { 1262 MarkerSegment seg = (MarkerSegment) iter.next(); 1263 if (seg instanceof DHTMarkerSegment) { 1264 oldDHTs.add(seg); 1265 } 1266 } 1267 if (!oldDHTs.isEmpty()) { 1268 NodeList children = node.getChildNodes(); 1269 for (int i = 0; i < children.getLength(); i++) { 1270 Node child = children.item(i); 1271 NamedNodeMap attrs = child.getAttributes(); 1272 int childID = MarkerSegment.getAttributeValue(child, 1273 attrs, 1274 "htableId", 1275 0, 3, 1276 true); 1277 int childClass = MarkerSegment.getAttributeValue(child, 1278 attrs, 1279 "class", 1280 0, 1, 1281 true); 1282 DHTMarkerSegment dht = null; 1283 int tableIndex = -1; 1284 for (int j = 0; j < oldDHTs.size(); j++) { 1285 DHTMarkerSegment testDHT = (DHTMarkerSegment) oldDHTs.get(j); 1286 for (int k = 0; k < testDHT.tables.size(); k++) { 1287 DHTMarkerSegment.Htable testTable = 1288 (DHTMarkerSegment.Htable) testDHT.tables.get(k); 1289 if ((childID == testTable.tableID) && 1290 (childClass == testTable.tableClass)) { 1291 dht = testDHT; 1292 tableIndex = k; 1293 break; 1294 } 1295 } 1296 if (dht != null) break; 1297 } 1298 if (dht != null) { 1299 dht.tables.set(tableIndex, dht.getHtableFromNode(child)); 1300 } else { 1301 dht = (DHTMarkerSegment) oldDHTs.get(oldDHTs.size()-1); 1302 dht.tables.add(dht.getHtableFromNode(child)); 1303 } 1304 } 1305 } else { 1306 DHTMarkerSegment newGuy = new DHTMarkerSegment(node); 1307 int lastDQT = findMarkerSegmentPosition(DQTMarkerSegment.class, false); 1308 int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); 1309 int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); 1310 if (lastDQT != -1) { 1311 markerSequence.add(lastDQT+1, newGuy); 1312 } else if (firstSOF != -1) { 1313 markerSequence.add(firstSOF, newGuy); 1314 } else if (firstSOS != -1) { 1315 markerSequence.add(firstSOS, newGuy); 1316 } else { 1317 markerSequence.add(newGuy); 1318 } 1319 } 1320 } 1321 1322 /** 1323 * Merge the given DRI node into the marker sequence. 1324 * If there already exists a DRI marker segment, the restart interval 1325 * value is updated. 1326 * If there is no DRI segment, then a new one is created and added as 1327 * follows: 1328 * If there is an SOF segment, the new DRI segment is inserted before 1329 * it. 1330 * If there is no SOF segment, the new DRI segment is inserted before 1331 * the first SOS segment, if there is one. 1332 * If there is no SOS segment, the new DRI segment is added to the end 1333 * of the sequence. 1334 */ 1335 private void mergeDRINode(Node node) throws IIOInvalidTreeException { 1336 DRIMarkerSegment dri = 1337 (DRIMarkerSegment) findMarkerSegment(DRIMarkerSegment.class, true); 1338 if (dri != null) { 1339 dri.updateFromNativeNode(node, false); 1340 } else { 1341 DRIMarkerSegment newGuy = new DRIMarkerSegment(node); 1342 int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); 1343 int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); 1344 if (firstSOF != -1) { 1345 markerSequence.add(firstSOF, newGuy); 1346 } else if (firstSOS != -1) { 1347 markerSequence.add(firstSOS, newGuy); 1348 } else { 1349 markerSequence.add(newGuy); 1350 } 1351 } 1352 } 1353 1354 /** 1355 * Merge the given COM node into the marker sequence. 1356 * A new COM marker segment is created and added to the sequence 1357 * using insertCOMMarkerSegment. 1358 */ 1359 private void mergeCOMNode(Node node) throws IIOInvalidTreeException { 1360 COMMarkerSegment newGuy = new COMMarkerSegment(node); 1361 insertCOMMarkerSegment(newGuy); 1362 } 1363 1364 /** 1365 * Insert a new COM marker segment into an appropriate place in the 1366 * marker sequence, as follows: 1367 * If there already exist COM marker segments, the new one is inserted 1368 * after the last one. 1369 * If there are no COM segments, the new COM segment is inserted after the 1370 * JFIF segment, if there is one. 1371 * If there is no JFIF segment, the new COM segment is inserted after the 1372 * Adobe marker segment, if there is one. 1373 * If there is no Adobe segment, the new COM segment is inserted 1374 * at the beginning of the sequence. 1375 */ 1376 private void insertCOMMarkerSegment(COMMarkerSegment newGuy) { 1377 int lastCOM = findMarkerSegmentPosition(COMMarkerSegment.class, false); 1378 boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null); 1379 int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true); 1380 if (lastCOM != -1) { 1381 markerSequence.add(lastCOM+1, newGuy); 1382 } else if (hasJFIF) { 1383 markerSequence.add(1, newGuy); // JFIF is always 0 1384 } else if (firstAdobe != -1) { 1385 markerSequence.add(firstAdobe+1, newGuy); 1386 } else { 1387 markerSequence.add(0, newGuy); 1388 } 1389 } 1390 1391 /** 1392 * Merge the given Adobe APP14 node into the marker sequence. 1393 * If there already exists an Adobe marker segment, then its attributes 1394 * are updated from the node. 1395 * If there is no Adobe segment, then a new one is created and added 1396 * using insertAdobeMarkerSegment. 1397 */ 1398 private void mergeAdobeNode(Node node) throws IIOInvalidTreeException { 1399 AdobeMarkerSegment adobe = 1400 (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); 1401 if (adobe != null) { 1402 adobe.updateFromNativeNode(node, false); 1403 } else { 1404 AdobeMarkerSegment newGuy = new AdobeMarkerSegment(node); 1405 insertAdobeMarkerSegment(newGuy); 1406 } 1407 } 1408 1409 /** 1410 * Insert the given AdobeMarkerSegment into the marker sequence, as 1411 * follows (we assume there is no Adobe segment yet): 1412 * If there is a JFIF segment, then the new Adobe segment is inserted 1413 * after it. 1414 * If there is no JFIF segment, the new Adobe segment is inserted after the 1415 * last Unknown segment, if there are any. 1416 * If there are no Unknown segments, the new Adobe segment is inserted 1417 * at the beginning of the sequence. 1418 */ 1419 private void insertAdobeMarkerSegment(AdobeMarkerSegment newGuy) { 1420 boolean hasJFIF = 1421 (findMarkerSegment(JFIFMarkerSegment.class, true) != null); 1422 int lastUnknown = findLastUnknownMarkerSegmentPosition(); 1423 if (hasJFIF) { 1424 markerSequence.add(1, newGuy); // JFIF is always 0 1425 } else if (lastUnknown != -1) { 1426 markerSequence.add(lastUnknown+1, newGuy); 1427 } else { 1428 markerSequence.add(0, newGuy); 1429 } 1430 } 1431 1432 /** 1433 * Merge the given Unknown node into the marker sequence. 1434 * A new Unknown marker segment is created and added to the sequence as 1435 * follows: 1436 * If there already exist Unknown marker segments, the new one is inserted 1437 * after the last one. 1438 * If there are no Unknown marker segments, the new Unknown marker segment 1439 * is inserted after the JFIF segment, if there is one. 1440 * If there is no JFIF segment, the new Unknown segment is inserted before 1441 * the Adobe marker segment, if there is one. 1442 * If there is no Adobe segment, the new Unknown segment is inserted 1443 * at the beginning of the sequence. 1444 */ 1445 private void mergeUnknownNode(Node node) throws IIOInvalidTreeException { 1446 MarkerSegment newGuy = new MarkerSegment(node); 1447 int lastUnknown = findLastUnknownMarkerSegmentPosition(); 1448 boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null); 1449 int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true); 1450 if (lastUnknown != -1) { 1451 markerSequence.add(lastUnknown+1, newGuy); 1452 } else if (hasJFIF) { 1453 markerSequence.add(1, newGuy); // JFIF is always 0 1454 } if (firstAdobe != -1) { 1455 markerSequence.add(firstAdobe, newGuy); 1456 } else { 1457 markerSequence.add(0, newGuy); 1458 } 1459 } 1460 1461 /** 1462 * Merge the given SOF node into the marker sequence. 1463 * If there already exists an SOF marker segment in the sequence, then 1464 * its values are updated from the node. 1465 * If there is no SOF segment, then a new one is created and added as 1466 * follows: 1467 * If there are any SOS segments, the new SOF segment is inserted before 1468 * the first one. 1469 * If there is no SOS segment, the new SOF segment is added to the end 1470 * of the sequence. 1471 * 1472 */ 1473 private void mergeSOFNode(Node node) throws IIOInvalidTreeException { 1474 SOFMarkerSegment sof = 1475 (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); 1476 if (sof != null) { 1477 sof.updateFromNativeNode(node, false); 1478 } else { 1479 SOFMarkerSegment newGuy = new SOFMarkerSegment(node); 1480 int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); 1481 if (firstSOS != -1) { 1482 markerSequence.add(firstSOS, newGuy); 1483 } else { 1484 markerSequence.add(newGuy); 1485 } 1486 } 1487 } 1488 1489 /** 1490 * Merge the given SOS node into the marker sequence. 1491 * If there already exists a single SOS marker segment, then the values 1492 * are updated from the node. 1493 * If there are more than one existing SOS marker segments, then an 1494 * IIOInvalidTreeException is thrown, as SOS segments cannot be merged 1495 * into a set of progressive scans. 1496 * If there are no SOS marker segments, a new one is created and added 1497 * to the end of the sequence. 1498 */ 1499 private void mergeSOSNode(Node node) throws IIOInvalidTreeException { 1500 SOSMarkerSegment firstSOS = 1501 (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true); 1502 SOSMarkerSegment lastSOS = 1503 (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, false); 1504 if (firstSOS != null) { 1505 if (firstSOS != lastSOS) { 1506 throw new IIOInvalidTreeException 1507 ("Can't merge SOS node into a tree with > 1 SOS node", node); 1508 } 1509 firstSOS.updateFromNativeNode(node, false); 1510 } else { 1511 markerSequence.add(new SOSMarkerSegment(node)); 1512 } 1513 } 1514 1515 private boolean transparencyDone; 1516 1517 private void mergeStandardTree(Node root) throws IIOInvalidTreeException { 1518 transparencyDone = false; 1519 NodeList children = root.getChildNodes(); 1520 for (int i = 0; i < children.getLength(); i++) { 1521 Node node = children.item(i); 1522 String name = node.getNodeName(); 1523 if (name.equals("Chroma")) { 1524 mergeStandardChromaNode(node, children); 1525 } else if (name.equals("Compression")) { 1526 mergeStandardCompressionNode(node); 1527 } else if (name.equals("Data")) { 1528 mergeStandardDataNode(node); 1529 } else if (name.equals("Dimension")) { 1530 mergeStandardDimensionNode(node); 1531 } else if (name.equals("Document")) { 1532 mergeStandardDocumentNode(node); 1533 } else if (name.equals("Text")) { 1534 mergeStandardTextNode(node); 1535 } else if (name.equals("Transparency")) { 1536 mergeStandardTransparencyNode(node); 1537 } else { 1538 throw new IIOInvalidTreeException("Invalid node: " + name, node); 1539 } 1540 } 1541 } 1542 1543 /* 1544 * In general, it could be possible to convert all non-pixel data to some 1545 * textual form and include it in comments, but then this would create the 1546 * expectation that these comment forms be recognized by the reader, thus 1547 * creating a defacto extension to JPEG metadata capabilities. This is 1548 * probably best avoided, so the following convert only text nodes to 1549 * comments, and lose the keywords as well. 1550 */ 1551 1552 private void mergeStandardChromaNode(Node node, NodeList siblings) 1553 throws IIOInvalidTreeException { 1554 // ColorSpaceType can change the target colorspace for compression 1555 // This must take any transparency node into account as well, as 1556 // that affects the number of channels (if alpha is present). If 1557 // a transparency node is dealt with here, set a flag to indicate 1558 // this to the transparency processor below. If we discover that 1559 // the nodes are not in order, throw an exception as the tree is 1560 // invalid. 1561 1562 if (transparencyDone) { 1563 throw new IIOInvalidTreeException 1564 ("Transparency node must follow Chroma node", node); 1565 } 1566 1567 Node csType = node.getFirstChild(); 1568 if ((csType == null) || !csType.getNodeName().equals("ColorSpaceType")) { 1569 // If there is no ColorSpaceType node, we have nothing to do 1570 return; 1571 } 1572 1573 String csName = csType.getAttributes().getNamedItem("name").getNodeValue(); 1574 1575 int numChannels = 0; 1576 boolean wantJFIF = false; 1577 boolean wantAdobe = false; 1578 int transform = 0; 1579 boolean willSubsample = false; 1580 byte [] ids = {1, 2, 3, 4}; // JFIF compatible 1581 if (csName.equals("GRAY")) { 1582 numChannels = 1; 1583 wantJFIF = true; 1584 } else if (csName.equals("YCbCr")) { 1585 numChannels = 3; 1586 wantJFIF = true; 1587 willSubsample = true; 1588 } else if (csName.equals("PhotoYCC")) { 1589 numChannels = 3; 1590 wantAdobe = true; 1591 transform = JPEG.ADOBE_YCC; 1592 ids[0] = (byte) 'Y'; 1593 ids[1] = (byte) 'C'; 1594 ids[2] = (byte) 'c'; 1595 } else if (csName.equals("RGB")) { 1596 numChannels = 3; 1597 wantAdobe = true; 1598 transform = JPEG.ADOBE_UNKNOWN; 1599 ids[0] = (byte) 'R'; 1600 ids[1] = (byte) 'G'; 1601 ids[2] = (byte) 'B'; 1602 } else if ((csName.equals("XYZ")) 1603 || (csName.equals("Lab")) 1604 || (csName.equals("Luv")) 1605 || (csName.equals("YxY")) 1606 || (csName.equals("HSV")) 1607 || (csName.equals("HLS")) 1608 || (csName.equals("CMY")) 1609 || (csName.equals("3CLR"))) { 1610 numChannels = 3; 1611 } else if (csName.equals("YCCK")) { 1612 numChannels = 4; 1613 wantAdobe = true; 1614 transform = JPEG.ADOBE_YCCK; 1615 willSubsample = true; 1616 } else if (csName.equals("CMYK")) { 1617 numChannels = 4; 1618 wantAdobe = true; 1619 transform = JPEG.ADOBE_UNKNOWN; 1620 } else if (csName.equals("4CLR")) { 1621 numChannels = 4; 1622 } else { // We can't handle them, so don't modify any metadata 1623 return; 1624 } 1625 1626 boolean wantAlpha = false; 1627 for (int i = 0; i < siblings.getLength(); i++) { 1628 Node trans = siblings.item(i); 1629 if (trans.getNodeName().equals("Transparency")) { 1630 wantAlpha = wantAlpha(trans); 1631 break; // out of for 1632 } 1633 } 1634 1635 if (wantAlpha) { 1636 numChannels++; 1637 wantJFIF = false; 1638 if (ids[0] == (byte) 'R') { 1639 ids[3] = (byte) 'A'; 1640 wantAdobe = false; 1641 } 1642 } 1643 1644 JFIFMarkerSegment jfif = 1645 (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); 1646 AdobeMarkerSegment adobe = 1647 (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); 1648 SOFMarkerSegment sof = 1649 (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); 1650 SOSMarkerSegment sos = 1651 (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true); 1652 1653 // If the metadata specifies progressive, then the number of channels 1654 // must match, so that we can modify all the existing SOS marker segments. 1655 // If they don't match, we don't know what to do with SOS so we can't do 1656 // the merge. We then just return silently. 1657 // An exception would not be appropriate. A warning might, but we have 1658 // nowhere to send it to. 1659 if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive 1660 if ((sof.componentSpecs.length != numChannels) && (sos != null)) { 1661 return; 1662 } 1663 } 1664 1665 // JFIF header might be removed 1666 if (!wantJFIF && (jfif != null)) { 1667 markerSequence.remove(jfif); 1668 } 1669 1670 // Now add a JFIF if we do want one, but only if it isn't stream metadata 1671 if (wantJFIF && !isStream) { 1672 markerSequence.add(0, new JFIFMarkerSegment()); 1673 } 1674 1675 // Adobe header might be removed or the transform modified, if it isn't 1676 // stream metadata 1677 if (wantAdobe) { 1678 if ((adobe == null) && !isStream) { 1679 adobe = new AdobeMarkerSegment(transform); 1680 insertAdobeMarkerSegment(adobe); 1681 } else { 1682 adobe.transform = transform; 1683 } 1684 } else if (adobe != null) { 1685 markerSequence.remove(adobe); 1686 } 1687 1688 boolean updateQtables = false; 1689 boolean updateHtables = false; 1690 1691 boolean progressive = false; 1692 1693 int [] subsampledSelectors = {0, 1, 1, 0 } ; 1694 int [] nonSubsampledSelectors = { 0, 0, 0, 0}; 1695 1696 int [] newTableSelectors = willSubsample 1697 ? subsampledSelectors 1698 : nonSubsampledSelectors; 1699 1700 // Keep the old componentSpecs array 1701 SOFMarkerSegment.ComponentSpec [] oldCompSpecs = null; 1702 // SOF might be modified 1703 if (sof != null) { 1704 oldCompSpecs = sof.componentSpecs; 1705 progressive = (sof.tag == JPEG.SOF2); 1706 // Now replace the SOF with a new one; it might be the same, but 1707 // this is easier. 1708 markerSequence.set(markerSequence.indexOf(sof), 1709 new SOFMarkerSegment(progressive, 1710 false, // we never need extended 1711 willSubsample, 1712 ids, 1713 numChannels)); 1714 1715 // Now suss out if subsampling changed and set the boolean for 1716 // updating the q tables 1717 // if the old componentSpec q table selectors don't match 1718 // the new ones, update the qtables. The new selectors are already 1719 // in place in the new SOF segment above. 1720 for (int i = 0; i < oldCompSpecs.length; i++) { 1721 if (oldCompSpecs[i].QtableSelector != newTableSelectors[i]) { 1722 updateQtables = true; 1723 } 1724 } 1725 1726 if (progressive) { 1727 // if the component ids are different, update all the existing scans 1728 // ignore Huffman tables 1729 boolean idsDiffer = false; 1730 for (int i = 0; i < oldCompSpecs.length; i++) { 1731 if (ids[i] != oldCompSpecs[i].componentId) { 1732 idsDiffer = true; 1733 } 1734 } 1735 if (idsDiffer) { 1736 // update the ids in each SOS marker segment 1737 for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { 1738 MarkerSegment seg = (MarkerSegment) iter.next(); 1739 if (seg instanceof SOSMarkerSegment) { 1740 SOSMarkerSegment target = (SOSMarkerSegment) seg; 1741 for (int i = 0; i < target.componentSpecs.length; i++) { 1742 int oldSelector = 1743 target.componentSpecs[i].componentSelector; 1744 // Find the position in the old componentSpecs array 1745 // of the old component with the old selector 1746 // and replace the component selector with the 1747 // new id at the same position, as these match 1748 // the new component specs array in the SOF created 1749 // above. 1750 for (int j = 0; j < oldCompSpecs.length; j++) { 1751 if (oldCompSpecs[j].componentId == oldSelector) { 1752 target.componentSpecs[i].componentSelector = 1753 ids[j]; 1754 } 1755 } 1756 } 1757 } 1758 } 1759 } 1760 } else { 1761 if (sos != null) { 1762 // htables - if the old htable selectors don't match the new ones, 1763 // update the tables. 1764 for (int i = 0; i < sos.componentSpecs.length; i++) { 1765 if ((sos.componentSpecs[i].dcHuffTable 1766 != newTableSelectors[i]) 1767 || (sos.componentSpecs[i].acHuffTable 1768 != newTableSelectors[i])) { 1769 updateHtables = true; 1770 } 1771 } 1772 1773 // Might be the same as the old one, but this is easier. 1774 markerSequence.set(markerSequence.indexOf(sos), 1775 new SOSMarkerSegment(willSubsample, 1776 ids, 1777 numChannels)); 1778 } 1779 } 1780 } else { 1781 // should be stream metadata if there isn't an SOF, but check it anyway 1782 if (isStream) { 1783 // update tables - routines below check if it's really necessary 1784 updateQtables = true; 1785 updateHtables = true; 1786 } 1787 } 1788 1789 if (updateQtables) { 1790 List tableSegments = new ArrayList(); 1791 for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { 1792 MarkerSegment seg = (MarkerSegment) iter.next(); 1793 if (seg instanceof DQTMarkerSegment) { 1794 tableSegments.add(seg); 1795 } 1796 } 1797 // If there are no tables, don't add them, as the metadata encodes an 1798 // abbreviated stream. 1799 // If we are not subsampling, we just need one, so don't do anything 1800 if (!tableSegments.isEmpty() && willSubsample) { 1801 // Is it really necessary? There should be at least 2 tables. 1802 // If there is only one, assume it's a scaled "standard" 1803 // luminance table, extract the scaling factor, and generate a 1804 // scaled "standard" chrominance table. 1805 1806 // Find the table with selector 1. 1807 boolean found = false; 1808 for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { 1809 DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next(); 1810 for (Iterator tabiter = testdqt.tables.iterator(); 1811 tabiter.hasNext();) { 1812 DQTMarkerSegment.Qtable tab = 1813 (DQTMarkerSegment.Qtable) tabiter.next(); 1814 if (tab.tableID == 1) { 1815 found = true; 1816 } 1817 } 1818 } 1819 if (!found) { 1820 // find the table with selector 0. There should be one. 1821 DQTMarkerSegment.Qtable table0 = null; 1822 for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { 1823 DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next(); 1824 for (Iterator tabiter = testdqt.tables.iterator(); 1825 tabiter.hasNext();) { 1826 DQTMarkerSegment.Qtable tab = 1827 (DQTMarkerSegment.Qtable) tabiter.next(); 1828 if (tab.tableID == 0) { 1829 table0 = tab; 1830 } 1831 } 1832 } 1833 1834 // Assuming that the table with id 0 is a luminance table, 1835 // compute a new chrominance table of the same quality and 1836 // add it to the last DQT segment 1837 DQTMarkerSegment dqt = 1838 (DQTMarkerSegment) tableSegments.get(tableSegments.size()-1); 1839 dqt.tables.add(dqt.getChromaForLuma(table0)); 1840 } 1841 } 1842 } 1843 1844 if (updateHtables) { 1845 List tableSegments = new ArrayList(); 1846 for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { 1847 MarkerSegment seg = (MarkerSegment) iter.next(); 1848 if (seg instanceof DHTMarkerSegment) { 1849 tableSegments.add(seg); 1850 } 1851 } 1852 // If there are no tables, don't add them, as the metadata encodes an 1853 // abbreviated stream. 1854 // If we are not subsampling, we just need one, so don't do anything 1855 if (!tableSegments.isEmpty() && willSubsample) { 1856 // Is it really necessary? There should be at least 2 dc and 2 ac 1857 // tables. If there is only one, add a 1858 // "standard " chrominance table. 1859 1860 // find a table with selector 1. AC/DC is irrelevant 1861 boolean found = false; 1862 for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { 1863 DHTMarkerSegment testdht = (DHTMarkerSegment) iter.next(); 1864 for (Iterator tabiter = testdht.tables.iterator(); 1865 tabiter.hasNext();) { 1866 DHTMarkerSegment.Htable tab = 1867 (DHTMarkerSegment.Htable) tabiter.next(); 1868 if (tab.tableID == 1) { 1869 found = true; 1870 } 1871 } 1872 } 1873 if (!found) { 1874 // Create new standard dc and ac chrominance tables and add them 1875 // to the last DHT segment 1876 DHTMarkerSegment lastDHT = 1877 (DHTMarkerSegment) tableSegments.get(tableSegments.size()-1); 1878 lastDHT.addHtable(JPEGHuffmanTable.StdDCLuminance, true, 1); 1879 lastDHT.addHtable(JPEGHuffmanTable.StdACLuminance, true, 1); 1880 } 1881 } 1882 } 1883 } 1884 1885 private boolean wantAlpha(Node transparency) { 1886 boolean returnValue = false; 1887 Node alpha = transparency.getFirstChild(); // Alpha must be first if present 1888 if (alpha.getNodeName().equals("Alpha")) { 1889 if (alpha.hasAttributes()) { 1890 String value = 1891 alpha.getAttributes().getNamedItem("value").getNodeValue(); 1892 if (!value.equals("none")) { 1893 returnValue = true; 1894 } 1895 } 1896 } 1897 transparencyDone = true; 1898 return returnValue; 1899 } 1900 1901 private void mergeStandardCompressionNode(Node node) 1902 throws IIOInvalidTreeException { 1903 // NumProgressiveScans is ignored. Progression must be enabled on the 1904 // ImageWriteParam. 1905 // No-op 1906 } 1907 1908 private void mergeStandardDataNode(Node node) 1909 throws IIOInvalidTreeException { 1910 // No-op 1911 } 1912 1913 private void mergeStandardDimensionNode(Node node) 1914 throws IIOInvalidTreeException { 1915 // Pixel Aspect Ratio or pixel size can be incorporated if there is, 1916 // or can be, a JFIF segment 1917 JFIFMarkerSegment jfif = 1918 (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); 1919 if (jfif == null) { 1920 // Can there be one? 1921 // Criteria: 1922 // SOF must be present with 1 or 3 channels, (stream metadata fails this) 1923 // Component ids must be JFIF compatible. 1924 boolean canHaveJFIF = false; 1925 SOFMarkerSegment sof = 1926 (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); 1927 if (sof != null) { 1928 int numChannels = sof.componentSpecs.length; 1929 if ((numChannels == 1) || (numChannels == 3)) { 1930 canHaveJFIF = true; // remaining tests are negative 1931 for (int i = 0; i < sof.componentSpecs.length; i++) { 1932 if (sof.componentSpecs[i].componentId != i+1) 1933 canHaveJFIF = false; 1934 } 1935 // if Adobe present, transform = ADOBE_UNKNOWN for 1-channel, 1936 // ADOBE_YCC for 3-channel. 1937 AdobeMarkerSegment adobe = 1938 (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, 1939 true); 1940 if (adobe != null) { 1941 if (adobe.transform != ((numChannels == 1) 1942 ? JPEG.ADOBE_UNKNOWN 1943 : JPEG.ADOBE_YCC)) { 1944 canHaveJFIF = false; 1945 } 1946 } 1947 } 1948 } 1949 // If so, create one and insert it into the sequence. Note that 1950 // default is just pixel ratio at 1:1 1951 if (canHaveJFIF) { 1952 jfif = new JFIFMarkerSegment(); 1953 markerSequence.add(0, jfif); 1954 } 1955 } 1956 if (jfif != null) { 1957 NodeList children = node.getChildNodes(); 1958 for (int i = 0; i < children.getLength(); i++) { 1959 Node child = children.item(i); 1960 NamedNodeMap attrs = child.getAttributes(); 1961 String name = child.getNodeName(); 1962 if (name.equals("PixelAspectRatio")) { 1963 String valueString = attrs.getNamedItem("value").getNodeValue(); 1964 float value = Float.parseFloat(valueString); 1965 Point p = findIntegerRatio(value); 1966 jfif.resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO; 1967 jfif.Xdensity = p.x; 1968 jfif.Xdensity = p.y; 1969 } else if (name.equals("HorizontalPixelSize")) { 1970 String valueString = attrs.getNamedItem("value").getNodeValue(); 1971 float value = Float.parseFloat(valueString); 1972 // Convert from mm/dot to dots/cm 1973 int dpcm = (int) Math.round(1.0/(value*10.0)); 1974 jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM; 1975 jfif.Xdensity = dpcm; 1976 } else if (name.equals("VerticalPixelSize")) { 1977 String valueString = attrs.getNamedItem("value").getNodeValue(); 1978 float value = Float.parseFloat(valueString); 1979 // Convert from mm/dot to dots/cm 1980 int dpcm = (int) Math.round(1.0/(value*10.0)); 1981 jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM; 1982 jfif.Ydensity = dpcm; 1983 } 1984 1985 } 1986 } 1987 } 1988 1989 /* 1990 * Return a pair of integers whose ratio (x/y) approximates the given 1991 * float value. 1992 */ 1993 private static Point findIntegerRatio(float value) { 1994 float epsilon = 0.005F; 1995 1996 // Normalize 1997 value = Math.abs(value); 1998 1999 // Deal with min case 2000 if (value <= epsilon) { 2001 return new Point(1, 255); 2002 } 2003 2004 // Deal with max case 2005 if (value >= 255) { 2006 return new Point(255, 1); 2007 } 2008 2009 // Remember if we invert 2010 boolean inverted = false; 2011 if (value < 1.0) { 2012 value = 1.0F/value; 2013 inverted = true; 2014 } 2015 2016 // First approximation 2017 int y = 1; 2018 int x = Math.round(value); 2019 2020 float ratio = (float) x; 2021 float delta = Math.abs(value - ratio); 2022 while (delta > epsilon) { // not close enough 2023 // Increment y and compute a new x 2024 y++; 2025 x = Math.round(y*value); 2026 ratio = (float)x/(float)y; 2027 delta = Math.abs(value - ratio); 2028 } 2029 return inverted ? new Point(y, x) : new Point(x, y); 2030 } 2031 2032 private void mergeStandardDocumentNode(Node node) 2033 throws IIOInvalidTreeException { 2034 // No-op 2035 } 2036 2037 private void mergeStandardTextNode(Node node) 2038 throws IIOInvalidTreeException { 2039 // Convert to comments. For the moment ignore the encoding issue. 2040 // Ignore keywords, language, and encoding (for the moment). 2041 // If compression tag is present, use only entries with "none". 2042 NodeList children = node.getChildNodes(); 2043 for (int i = 0; i < children.getLength(); i++) { 2044 Node child = children.item(i); 2045 NamedNodeMap attrs = child.getAttributes(); 2046 Node comp = attrs.getNamedItem("compression"); 2047 boolean copyIt = true; 2048 if (comp != null) { 2049 String compString = comp.getNodeValue(); 2050 if (!compString.equals("none")) { 2051 copyIt = false; 2052 } 2053 } 2054 if (copyIt) { 2055 String value = attrs.getNamedItem("value").getNodeValue(); 2056 COMMarkerSegment com = new COMMarkerSegment(value); 2057 insertCOMMarkerSegment(com); 2058 } 2059 } 2060 } 2061 2062 private void mergeStandardTransparencyNode(Node node) 2063 throws IIOInvalidTreeException { 2064 // This might indicate that an alpha channel is being added or removed. 2065 // The nodes must appear in order, and a Chroma node will process any 2066 // transparency, so process it here only if there was no Chroma node 2067 // Do nothing for stream metadata 2068 if (!transparencyDone && !isStream) { 2069 boolean wantAlpha = wantAlpha(node); 2070 // do we have alpha already? If the number of channels is 2 or 4, 2071 // we do, as we don't support CMYK, nor can we add alpha to it 2072 // The number of channels can be determined from the SOF 2073 JFIFMarkerSegment jfif = (JFIFMarkerSegment) findMarkerSegment 2074 (JFIFMarkerSegment.class, true); 2075 AdobeMarkerSegment adobe = (AdobeMarkerSegment) findMarkerSegment 2076 (AdobeMarkerSegment.class, true); 2077 SOFMarkerSegment sof = (SOFMarkerSegment) findMarkerSegment 2078 (SOFMarkerSegment.class, true); 2079 SOSMarkerSegment sos = (SOSMarkerSegment) findMarkerSegment 2080 (SOSMarkerSegment.class, true); 2081 2082 // We can do nothing for progressive, as we don't know how to 2083 // modify the scans. 2084 if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive 2085 return; 2086 } 2087 2088 // Do we already have alpha? We can tell by the number of channels 2089 // We must have an sof, or we can't do anything further 2090 if (sof != null) { 2091 int numChannels = sof.componentSpecs.length; 2092 boolean hadAlpha = (numChannels == 2) || (numChannels == 4); 2093 // proceed only if the old state and the new state differ 2094 if (hadAlpha != wantAlpha) { 2095 if (wantAlpha) { // Adding alpha 2096 numChannels++; 2097 if (jfif != null) { 2098 markerSequence.remove(jfif); 2099 } 2100 2101 // If an adobe marker is present, transform must be UNKNOWN 2102 if (adobe != null) { 2103 adobe.transform = JPEG.ADOBE_UNKNOWN; 2104 } 2105 2106 // Add a component spec with appropriate parameters to SOF 2107 SOFMarkerSegment.ComponentSpec [] newSpecs = 2108 new SOFMarkerSegment.ComponentSpec[numChannels]; 2109 for (int i = 0; i < sof.componentSpecs.length; i++) { 2110 newSpecs[i] = sof.componentSpecs[i]; 2111 } 2112 byte oldFirstID = (byte) sof.componentSpecs[0].componentId; 2113 byte newID = (byte) ((oldFirstID > 1) ? 'A' : 4); 2114 newSpecs[numChannels-1] = 2115 sof.getComponentSpec(newID, 2116 sof.componentSpecs[0].HsamplingFactor, 2117 sof.componentSpecs[0].QtableSelector); 2118 2119 sof.componentSpecs = newSpecs; 2120 2121 // Add a component spec with appropriate parameters to SOS 2122 SOSMarkerSegment.ScanComponentSpec [] newScanSpecs = 2123 new SOSMarkerSegment.ScanComponentSpec [numChannels]; 2124 for (int i = 0; i < sos.componentSpecs.length; i++) { 2125 newScanSpecs[i] = sos.componentSpecs[i]; 2126 } 2127 newScanSpecs[numChannels-1] = 2128 sos.getScanComponentSpec (newID, 0); 2129 sos.componentSpecs = newScanSpecs; 2130 } else { // Removing alpha 2131 numChannels--; 2132 // Remove a component spec from SOF 2133 SOFMarkerSegment.ComponentSpec [] newSpecs = 2134 new SOFMarkerSegment.ComponentSpec[numChannels]; 2135 for (int i = 0; i < numChannels; i++) { 2136 newSpecs[i] = sof.componentSpecs[i]; 2137 } 2138 sof.componentSpecs = newSpecs; 2139 2140 // Remove a component spec from SOS 2141 SOSMarkerSegment.ScanComponentSpec [] newScanSpecs = 2142 new SOSMarkerSegment.ScanComponentSpec [numChannels]; 2143 for (int i = 0; i < numChannels; i++) { 2144 newScanSpecs[i] = sos.componentSpecs[i]; 2145 } 2146 sos.componentSpecs = newScanSpecs; 2147 } 2148 } 2149 } 2150 } 2151 } 2152 2153 2154 public void setFromTree(String formatName, Node root) 2155 throws IIOInvalidTreeException { 2156 if (formatName == null) { 2157 throw new IllegalArgumentException("null formatName!"); 2158 } 2159 if (root == null) { 2160 throw new IllegalArgumentException("null root!"); 2161 } 2162 if (isStream && 2163 (formatName.equals(JPEG.nativeStreamMetadataFormatName))) { 2164 setFromNativeTree(root); 2165 } else if (!isStream && 2166 (formatName.equals(JPEG.nativeImageMetadataFormatName))) { 2167 setFromNativeTree(root); 2168 } else if (!isStream && 2169 (formatName.equals 2170 (IIOMetadataFormatImpl.standardMetadataFormatName))) { 2171 // In this case a reset followed by a merge is correct 2172 super.setFromTree(formatName, root); 2173 } else { 2174 throw new IllegalArgumentException("Unsupported format name: " 2175 + formatName); 2176 } 2177 } 2178 2179 private void setFromNativeTree(Node root) throws IIOInvalidTreeException { 2180 if (resetSequence == null) { 2181 resetSequence = markerSequence; 2182 } 2183 markerSequence = new ArrayList(); 2184 2185 // Build a whole new marker sequence from the tree 2186 2187 String name = root.getNodeName(); 2188 if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName 2189 : JPEG.nativeImageMetadataFormatName)) { 2190 throw new IIOInvalidTreeException("Invalid root node name: " + name, 2191 root); 2192 } 2193 if (!isStream) { 2194 if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence 2195 throw new IIOInvalidTreeException( 2196 "JPEGvariety and markerSequence nodes must be present", root); 2197 } 2198 2199 Node JPEGvariety = root.getFirstChild(); 2200 2201 if (JPEGvariety.getChildNodes().getLength() != 0) { 2202 markerSequence.add(new JFIFMarkerSegment(JPEGvariety.getFirstChild())); 2203 } 2204 } 2205 2206 Node markerSequenceNode = isStream ? root : root.getLastChild(); 2207 setFromMarkerSequenceNode(markerSequenceNode); 2208 2209 } 2210 2211 void setFromMarkerSequenceNode(Node markerSequenceNode) 2212 throws IIOInvalidTreeException{ 2213 2214 NodeList children = markerSequenceNode.getChildNodes(); 2215 // for all the children, add a marker segment 2216 for (int i = 0; i < children.getLength(); i++) { 2217 Node node = children.item(i); 2218 String childName = node.getNodeName(); 2219 if (childName.equals("dqt")) { 2220 markerSequence.add(new DQTMarkerSegment(node)); 2221 } else if (childName.equals("dht")) { 2222 markerSequence.add(new DHTMarkerSegment(node)); 2223 } else if (childName.equals("dri")) { 2224 markerSequence.add(new DRIMarkerSegment(node)); 2225 } else if (childName.equals("com")) { 2226 markerSequence.add(new COMMarkerSegment(node)); 2227 } else if (childName.equals("app14Adobe")) { 2228 markerSequence.add(new AdobeMarkerSegment(node)); 2229 } else if (childName.equals("unknown")) { 2230 markerSequence.add(new MarkerSegment(node)); 2231 } else if (childName.equals("sof")) { 2232 markerSequence.add(new SOFMarkerSegment(node)); 2233 } else if (childName.equals("sos")) { 2234 markerSequence.add(new SOSMarkerSegment(node)); 2235 } else { 2236 throw new IIOInvalidTreeException("Invalid " 2237 + (isStream ? "stream " : "image ") + "child: " 2238 + childName, node); 2239 } 2240 } 2241 } 2242 2243 /** 2244 * Check that this metadata object is in a consistent state and 2245 * return <code>true</code> if it is or <code>false</code> 2246 * otherwise. All the constructors and modifiers should call 2247 * this method at the end to guarantee that the data is always 2248 * consistent, as the writer relies on this. 2249 */ 2250 private boolean isConsistent() { 2251 SOFMarkerSegment sof = 2252 (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, 2253 true); 2254 JFIFMarkerSegment jfif = 2255 (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, 2256 true); 2257 AdobeMarkerSegment adobe = 2258 (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, 2259 true); 2260 boolean retval = true; 2261 if (!isStream) { 2262 if (sof != null) { 2263 // SOF numBands = total scan bands 2264 int numSOFBands = sof.componentSpecs.length; 2265 int numScanBands = countScanBands(); 2266 if (numScanBands != 0) { // No SOS is OK 2267 if (numScanBands != numSOFBands) { 2268 retval = false; 2269 } 2270 } 2271 // If JFIF is present, component ids are 1-3, bands are 1 or 3 2272 if (jfif != null) { 2273 if ((numSOFBands != 1) && (numSOFBands != 3)) { 2274 retval = false; 2275 } 2276 for (int i = 0; i < numSOFBands; i++) { 2277 if (sof.componentSpecs[i].componentId != i+1) { 2278 retval = false; 2279 } 2280 } 2281 2282 // If both JFIF and Adobe are present, 2283 // Adobe transform == unknown for gray, 2284 // YCC for 3-chan. 2285 if ((adobe != null) 2286 && (((numSOFBands == 1) 2287 && (adobe.transform != JPEG.ADOBE_UNKNOWN)) 2288 || ((numSOFBands == 3) 2289 && (adobe.transform != JPEG.ADOBE_YCC)))) { 2290 retval = false; 2291 } 2292 } 2293 } else { 2294 // stream can't have jfif, adobe, sof, or sos 2295 SOSMarkerSegment sos = 2296 (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, 2297 true); 2298 if ((jfif != null) || (adobe != null) 2299 || (sof != null) || (sos != null)) { 2300 retval = false; 2301 } 2302 } 2303 } 2304 return retval; 2305 } 2306 2307 /** 2308 * Returns the total number of bands referenced in all SOS marker 2309 * segments, including 0 if there are no SOS marker segments. 2310 */ 2311 private int countScanBands() { 2312 List ids = new ArrayList(); 2313 Iterator iter = markerSequence.iterator(); 2314 while(iter.hasNext()) { 2315 MarkerSegment seg = (MarkerSegment)iter.next(); 2316 if (seg instanceof SOSMarkerSegment) { 2317 SOSMarkerSegment sos = (SOSMarkerSegment) seg; 2318 SOSMarkerSegment.ScanComponentSpec [] specs = sos.componentSpecs; 2319 for (int i = 0; i < specs.length; i++) { 2320 Integer id = new Integer(specs[i].componentSelector); 2321 if (!ids.contains(id)) { 2322 ids.add(id); 2323 } 2324 } 2325 } 2326 } 2327 2328 return ids.size(); 2329 } 2330 2331 ///// Writer support 2332 2333 void writeToStream(ImageOutputStream ios, 2334 boolean ignoreJFIF, 2335 boolean forceJFIF, 2336 List thumbnails, 2337 ICC_Profile iccProfile, 2338 boolean ignoreAdobe, 2339 int newAdobeTransform, 2340 JPEGImageWriter writer) 2341 throws IOException { 2342 if (forceJFIF) { 2343 // Write a default JFIF segment, including thumbnails 2344 // This won't be duplicated below because forceJFIF will be 2345 // set only if there is no JFIF present already. 2346 JFIFMarkerSegment.writeDefaultJFIF(ios, 2347 thumbnails, 2348 iccProfile, 2349 writer); 2350 if ((ignoreAdobe == false) 2351 && (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE)) { 2352 if ((newAdobeTransform != JPEG.ADOBE_UNKNOWN) 2353 && (newAdobeTransform != JPEG.ADOBE_YCC)) { 2354 // Not compatible, so ignore Adobe. 2355 ignoreAdobe = true; 2356 writer.warningOccurred 2357 (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB); 2358 } 2359 } 2360 } 2361 // Iterate over each MarkerSegment 2362 Iterator iter = markerSequence.iterator(); 2363 while(iter.hasNext()) { 2364 MarkerSegment seg = (MarkerSegment)iter.next(); 2365 if (seg instanceof JFIFMarkerSegment) { 2366 if (ignoreJFIF == false) { 2367 JFIFMarkerSegment jfif = (JFIFMarkerSegment) seg; 2368 jfif.writeWithThumbs(ios, thumbnails, writer); 2369 if (iccProfile != null) { 2370 JFIFMarkerSegment.writeICC(iccProfile, ios); 2371 } 2372 } // Otherwise ignore it, as requested 2373 } else if (seg instanceof AdobeMarkerSegment) { 2374 if (ignoreAdobe == false) { 2375 if (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE) { 2376 AdobeMarkerSegment newAdobe = 2377 (AdobeMarkerSegment) seg.clone(); 2378 newAdobe.transform = newAdobeTransform; 2379 newAdobe.write(ios); 2380 } else if (forceJFIF) { 2381 // If adobe isn't JFIF compatible, ignore it 2382 AdobeMarkerSegment adobe = (AdobeMarkerSegment) seg; 2383 if ((adobe.transform == JPEG.ADOBE_UNKNOWN) 2384 || (adobe.transform == JPEG.ADOBE_YCC)) { 2385 adobe.write(ios); 2386 } else { 2387 writer.warningOccurred 2388 (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB); 2389 } 2390 } else { 2391 seg.write(ios); 2392 } 2393 } // Otherwise ignore it, as requested 2394 } else { 2395 seg.write(ios); 2396 } 2397 } 2398 } 2399 2400 //// End of writer support 2401 2402 public void reset() { 2403 if (resetSequence != null) { // Otherwise no need to reset 2404 markerSequence = resetSequence; 2405 resetSequence = null; 2406 } 2407 } 2408 2409 public void print() { 2410 for (int i = 0; i < markerSequence.size(); i++) { 2411 MarkerSegment seg = (MarkerSegment) markerSequence.get(i); 2412 seg.print(); 2413 } 2414 } 2415 2416 }