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