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