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