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