| /* |
| * Copyright (c) 2001, 2004, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package com.sun.imageio.plugins.jpeg; |
| |
| import javax.imageio.ImageTypeSpecifier; |
| import javax.imageio.ImageWriteParam; |
| import javax.imageio.IIOException; |
| import javax.imageio.stream.ImageInputStream; |
| import javax.imageio.stream.ImageOutputStream; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.metadata.IIOMetadataNode; |
| import javax.imageio.metadata.IIOMetadataFormat; |
| import javax.imageio.metadata.IIOMetadataFormatImpl; |
| import javax.imageio.metadata.IIOInvalidTreeException; |
| import javax.imageio.plugins.jpeg.JPEGQTable; |
| import javax.imageio.plugins.jpeg.JPEGHuffmanTable; |
| import javax.imageio.plugins.jpeg.JPEGImageWriteParam; |
| |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.w3c.dom.NamedNodeMap; |
| |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.ListIterator; |
| import java.io.IOException; |
| import java.awt.color.ICC_Profile; |
| import java.awt.color.ICC_ColorSpace; |
| import java.awt.color.ColorSpace; |
| import java.awt.image.ColorModel; |
| import java.awt.Point; |
| |
| /** |
| * Metadata for the JPEG plug-in. |
| */ |
| public class JPEGMetadata extends IIOMetadata implements Cloneable { |
| |
| //////// Private variables |
| |
| private static final boolean debug = false; |
| |
| /** |
| * A copy of <code>markerSequence</code>, created the first time the |
| * <code>markerSequence</code> is modified. This is used by reset |
| * to restore the original state. |
| */ |
| private List resetSequence = null; |
| |
| /** |
| * Set to <code>true</code> when reading a thumbnail stored as |
| * JPEG. This is used to enforce the prohibition of JFIF thumbnails |
| * containing any JFIF marker segments, and to ensure generation of |
| * a correct native subtree during <code>getAsTree</code>. |
| */ |
| private boolean inThumb = false; |
| |
| /** |
| * Set by the chroma node construction method to signal the |
| * presence or absence of an alpha channel to the transparency |
| * node construction method. Used only when constructing a |
| * standard metadata tree. |
| */ |
| private boolean hasAlpha; |
| |
| //////// end of private variables |
| |
| /////// Package-access variables |
| |
| /** |
| * All data is a list of <code>MarkerSegment</code> objects. |
| * When accessing the list, use the tag to identify the particular |
| * subclass. Any JFIF marker segment must be the first element |
| * of the list if it is present, and any JFXX or APP2ICC marker |
| * segments are subordinate to the JFIF marker segment. This |
| * list is package visible so that the writer can access it. |
| * @see #MarkerSegment |
| */ |
| List markerSequence = new ArrayList(); |
| |
| /** |
| * Indicates whether this object represents stream or image |
| * metadata. Package-visible so the writer can see it. |
| */ |
| final boolean isStream; |
| |
| /////// End of package-access variables |
| |
| /////// Constructors |
| |
| /** |
| * Constructor containing code shared by other constructors. |
| */ |
| JPEGMetadata(boolean isStream, boolean inThumb) { |
| super(true, // Supports standard format |
| JPEG.nativeImageMetadataFormatName, // and a native format |
| JPEG.nativeImageMetadataFormatClassName, |
| null, null); // No other formats |
| this.inThumb = inThumb; |
| // But if we are stream metadata, adjust the variables |
| this.isStream = isStream; |
| if (isStream) { |
| nativeMetadataFormatName = JPEG.nativeStreamMetadataFormatName; |
| nativeMetadataFormatClassName = |
| JPEG.nativeStreamMetadataFormatClassName; |
| } |
| } |
| |
| /* |
| * Constructs a <code>JPEGMetadata</code> object by reading the |
| * contents of an <code>ImageInputStream</code>. Has package-only |
| * access. |
| * |
| * @param isStream A boolean indicating whether this object will be |
| * stream or image metadata. |
| * @param isThumb A boolean indicating whether this metadata object |
| * is for an image or for a thumbnail stored as JPEG. |
| * @param iis An <code>ImageInputStream</code> from which to read |
| * the metadata. |
| * @param reader The <code>JPEGImageReader</code> calling this |
| * constructor, to which warnings should be sent. |
| */ |
| JPEGMetadata(boolean isStream, |
| boolean isThumb, |
| ImageInputStream iis, |
| JPEGImageReader reader) throws IOException { |
| this(isStream, isThumb); |
| |
| JPEGBuffer buffer = new JPEGBuffer(iis); |
| |
| buffer.loadBuf(0); |
| |
| // The first three bytes should be FF, SOI, FF |
| if (((buffer.buf[0] & 0xff) != 0xff) |
| || ((buffer.buf[1] & 0xff) != JPEG.SOI) |
| || ((buffer.buf[2] & 0xff) != 0xff)) { |
| throw new IIOException ("Image format error"); |
| } |
| |
| boolean done = false; |
| buffer.bufAvail -=2; // Next byte should be the ff before a marker |
| buffer.bufPtr = 2; |
| MarkerSegment newGuy = null; |
| while (!done) { |
| byte [] buf; |
| int ptr; |
| buffer.loadBuf(1); |
| if (debug) { |
| System.out.println("top of loop"); |
| buffer.print(10); |
| } |
| buffer.scanForFF(reader); |
| switch (buffer.buf[buffer.bufPtr] & 0xff) { |
| case 0: |
| if (debug) { |
| System.out.println("Skipping 0"); |
| } |
| buffer.bufAvail--; |
| buffer.bufPtr++; |
| break; |
| case JPEG.SOF0: |
| case JPEG.SOF1: |
| case JPEG.SOF2: |
| if (isStream) { |
| throw new IIOException |
| ("SOF not permitted in stream metadata"); |
| } |
| newGuy = new SOFMarkerSegment(buffer); |
| break; |
| case JPEG.DQT: |
| newGuy = new DQTMarkerSegment(buffer); |
| break; |
| case JPEG.DHT: |
| newGuy = new DHTMarkerSegment(buffer); |
| break; |
| case JPEG.DRI: |
| newGuy = new DRIMarkerSegment(buffer); |
| break; |
| case JPEG.APP0: |
| // Either JFIF, JFXX, or unknown APP0 |
| buffer.loadBuf(8); // tag, length, id |
| buf = buffer.buf; |
| ptr = buffer.bufPtr; |
| if ((buf[ptr+3] == 'J') |
| && (buf[ptr+4] == 'F') |
| && (buf[ptr+5] == 'I') |
| && (buf[ptr+6] == 'F') |
| && (buf[ptr+7] == 0)) { |
| if (inThumb) { |
| reader.warningOccurred |
| (JPEGImageReader.WARNING_NO_JFIF_IN_THUMB); |
| // Leave newGuy null |
| // Read a dummy to skip the segment |
| JFIFMarkerSegment dummy = |
| new JFIFMarkerSegment(buffer); |
| } else if (isStream) { |
| throw new IIOException |
| ("JFIF not permitted in stream metadata"); |
| } else if (markerSequence.isEmpty() == false) { |
| throw new IIOException |
| ("JFIF APP0 must be first marker after SOI"); |
| } else { |
| newGuy = new JFIFMarkerSegment(buffer); |
| } |
| } else if ((buf[ptr+3] == 'J') |
| && (buf[ptr+4] == 'F') |
| && (buf[ptr+5] == 'X') |
| && (buf[ptr+6] == 'X') |
| && (buf[ptr+7] == 0)) { |
| if (isStream) { |
| throw new IIOException |
| ("JFXX not permitted in stream metadata"); |
| } |
| if (inThumb) { |
| throw new IIOException |
| ("JFXX markers not allowed in JFIF JPEG thumbnail"); |
| } |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| if (jfif == null) { |
| throw new IIOException |
| ("JFXX encountered without prior JFIF!"); |
| } |
| jfif.addJFXX(buffer, reader); |
| // newGuy remains null |
| } else { |
| newGuy = new MarkerSegment(buffer); |
| newGuy.loadData(buffer); |
| } |
| break; |
| case JPEG.APP2: |
| // Either an ICC profile or unknown APP2 |
| buffer.loadBuf(15); // tag, length, id |
| if ((buffer.buf[buffer.bufPtr+3] == 'I') |
| && (buffer.buf[buffer.bufPtr+4] == 'C') |
| && (buffer.buf[buffer.bufPtr+5] == 'C') |
| && (buffer.buf[buffer.bufPtr+6] == '_') |
| && (buffer.buf[buffer.bufPtr+7] == 'P') |
| && (buffer.buf[buffer.bufPtr+8] == 'R') |
| && (buffer.buf[buffer.bufPtr+9] == 'O') |
| && (buffer.buf[buffer.bufPtr+10] == 'F') |
| && (buffer.buf[buffer.bufPtr+11] == 'I') |
| && (buffer.buf[buffer.bufPtr+12] == 'L') |
| && (buffer.buf[buffer.bufPtr+13] == 'E') |
| && (buffer.buf[buffer.bufPtr+14] == 0) |
| ) { |
| if (isStream) { |
| throw new IIOException |
| ("ICC profiles not permitted in stream metadata"); |
| } |
| |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| if (jfif == null) { |
| throw new IIOException |
| ("ICC APP2 encountered without prior JFIF!"); |
| } |
| jfif.addICC(buffer); |
| // newGuy remains null |
| } else { |
| newGuy = new MarkerSegment(buffer); |
| newGuy.loadData(buffer); |
| } |
| break; |
| case JPEG.APP14: |
| // Either Adobe or unknown APP14 |
| buffer.loadBuf(8); // tag, length, id |
| if ((buffer.buf[buffer.bufPtr+3] == 'A') |
| && (buffer.buf[buffer.bufPtr+4] == 'd') |
| && (buffer.buf[buffer.bufPtr+5] == 'o') |
| && (buffer.buf[buffer.bufPtr+6] == 'b') |
| && (buffer.buf[buffer.bufPtr+7] == 'e')) { |
| if (isStream) { |
| throw new IIOException |
| ("Adobe APP14 markers not permitted in stream metadata"); |
| } |
| newGuy = new AdobeMarkerSegment(buffer); |
| } else { |
| newGuy = new MarkerSegment(buffer); |
| newGuy.loadData(buffer); |
| } |
| |
| break; |
| case JPEG.COM: |
| newGuy = new COMMarkerSegment(buffer); |
| break; |
| case JPEG.SOS: |
| if (isStream) { |
| throw new IIOException |
| ("SOS not permitted in stream metadata"); |
| } |
| newGuy = new SOSMarkerSegment(buffer); |
| break; |
| case JPEG.RST0: |
| case JPEG.RST1: |
| case JPEG.RST2: |
| case JPEG.RST3: |
| case JPEG.RST4: |
| case JPEG.RST5: |
| case JPEG.RST6: |
| case JPEG.RST7: |
| if (debug) { |
| System.out.println("Restart Marker"); |
| } |
| buffer.bufPtr++; // Just skip it |
| buffer.bufAvail--; |
| break; |
| case JPEG.EOI: |
| done = true; |
| buffer.bufPtr++; |
| buffer.bufAvail--; |
| break; |
| default: |
| newGuy = new MarkerSegment(buffer); |
| newGuy.loadData(buffer); |
| newGuy.unknown = true; |
| break; |
| } |
| if (newGuy != null) { |
| markerSequence.add(newGuy); |
| if (debug) { |
| newGuy.print(); |
| } |
| newGuy = null; |
| } |
| } |
| |
| // Now that we've read up to the EOI, we need to push back |
| // whatever is left in the buffer, so that the next read |
| // in the native code will work. |
| |
| buffer.pushBack(); |
| |
| if (!isConsistent()) { |
| throw new IIOException("Inconsistent metadata read from stream"); |
| } |
| } |
| |
| /** |
| * Constructs a default stream <code>JPEGMetadata</code> object appropriate |
| * for the given write parameters. |
| */ |
| JPEGMetadata(ImageWriteParam param, JPEGImageWriter writer) { |
| this(true, false); |
| |
| JPEGImageWriteParam jparam = null; |
| |
| if ((param != null) && (param instanceof JPEGImageWriteParam)) { |
| jparam = (JPEGImageWriteParam) param; |
| if (!jparam.areTablesSet()) { |
| jparam = null; |
| } |
| } |
| if (jparam != null) { |
| markerSequence.add(new DQTMarkerSegment(jparam.getQTables())); |
| markerSequence.add |
| (new DHTMarkerSegment(jparam.getDCHuffmanTables(), |
| jparam.getACHuffmanTables())); |
| } else { |
| // default tables. |
| markerSequence.add(new DQTMarkerSegment(JPEG.getDefaultQTables())); |
| markerSequence.add(new DHTMarkerSegment(JPEG.getDefaultHuffmanTables(true), |
| JPEG.getDefaultHuffmanTables(false))); |
| } |
| |
| // Defensive programming |
| if (!isConsistent()) { |
| throw new InternalError("Default stream metadata is inconsistent"); |
| } |
| } |
| |
| /** |
| * Constructs a default image <code>JPEGMetadata</code> object appropriate |
| * for the given image type and write parameters. |
| */ |
| JPEGMetadata(ImageTypeSpecifier imageType, |
| ImageWriteParam param, |
| JPEGImageWriter writer) { |
| this(false, false); |
| |
| boolean wantJFIF = true; |
| boolean wantAdobe = false; |
| int transform = JPEG.ADOBE_UNKNOWN; |
| boolean willSubsample = true; |
| boolean wantICC = false; |
| boolean wantProg = false; |
| boolean wantOptimized = false; |
| boolean wantExtended = false; |
| boolean wantQTables = true; |
| boolean wantHTables = true; |
| float quality = JPEG.DEFAULT_QUALITY; |
| byte[] componentIDs = { 1, 2, 3, 4}; |
| int numComponents = 0; |
| |
| ImageTypeSpecifier destType = null; |
| |
| if (param != null) { |
| destType = param.getDestinationType(); |
| if (destType != null) { |
| if (imageType != null) { |
| // Ignore the destination type. |
| writer.warningOccurred |
| (JPEGImageWriter.WARNING_DEST_IGNORED); |
| destType = null; |
| } |
| } |
| // The only progressive mode that makes sense here is MODE_DEFAULT |
| if (param.canWriteProgressive()) { |
| // the param may not be one of ours, so it may return false. |
| // If so, the following would throw an exception |
| if (param.getProgressiveMode() == ImageWriteParam.MODE_DEFAULT) { |
| wantProg = true; |
| wantOptimized = true; |
| wantHTables = false; |
| } |
| } |
| |
| if (param instanceof JPEGImageWriteParam) { |
| JPEGImageWriteParam jparam = (JPEGImageWriteParam) param; |
| if (jparam.areTablesSet()) { |
| wantQTables = false; // If the param has them, metadata shouldn't |
| wantHTables = false; |
| if ((jparam.getDCHuffmanTables().length > 2) |
| || (jparam.getACHuffmanTables().length > 2)) { |
| wantExtended = true; |
| } |
| } |
| // Progressive forces optimized, regardless of param setting |
| // so consult the param re optimized only if not progressive |
| if (!wantProg) { |
| wantOptimized = jparam.getOptimizeHuffmanTables(); |
| if (wantOptimized) { |
| wantHTables = false; |
| } |
| } |
| } |
| |
| // compression quality should determine the q tables. Note that this |
| // will be ignored if we already decided not to create any. |
| // Again, the param may not be one of ours, so we must check that it |
| // supports compression settings |
| if (param.canWriteCompressed()) { |
| if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { |
| quality = param.getCompressionQuality(); |
| } |
| } |
| } |
| |
| // We are done with the param, now for the image types |
| |
| ColorSpace cs = null; |
| if (destType != null) { |
| ColorModel cm = destType.getColorModel(); |
| numComponents = cm.getNumComponents(); |
| boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents); |
| boolean hasAlpha = cm.hasAlpha(); |
| cs = cm.getColorSpace(); |
| int type = cs.getType(); |
| switch(type) { |
| case ColorSpace.TYPE_GRAY: |
| willSubsample = false; |
| if (hasExtraComponents) { // e.g. alpha |
| wantJFIF = false; |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| if (cs == JPEG.JCS.getYCC()) { |
| wantJFIF = false; |
| componentIDs[0] = (byte) 'Y'; |
| componentIDs[1] = (byte) 'C'; |
| componentIDs[2] = (byte) 'c'; |
| if (hasAlpha) { |
| componentIDs[3] = (byte) 'A'; |
| } |
| } |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| if (hasExtraComponents) { // e.g. K or alpha |
| wantJFIF = false; |
| if (!hasAlpha) { // Not alpha, so must be K |
| wantAdobe = true; |
| transform = JPEG.ADOBE_YCCK; |
| } |
| } |
| break; |
| case ColorSpace.TYPE_RGB: // with or without alpha |
| wantJFIF = false; |
| wantAdobe = true; |
| willSubsample = false; |
| componentIDs[0] = (byte) 'R'; |
| componentIDs[1] = (byte) 'G'; |
| componentIDs[2] = (byte) 'B'; |
| if (hasAlpha) { |
| componentIDs[3] = (byte) 'A'; |
| } |
| break; |
| default: |
| // Everything else is not subsampled, gets no special marker, |
| // and component ids are 1 - N |
| wantJFIF = false; |
| willSubsample = false; |
| } |
| } else if (imageType != null) { |
| ColorModel cm = imageType.getColorModel(); |
| numComponents = cm.getNumComponents(); |
| boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents); |
| boolean hasAlpha = cm.hasAlpha(); |
| cs = cm.getColorSpace(); |
| int type = cs.getType(); |
| switch(type) { |
| case ColorSpace.TYPE_GRAY: |
| willSubsample = false; |
| if (hasExtraComponents) { // e.g. alpha |
| wantJFIF = false; |
| } |
| break; |
| case ColorSpace.TYPE_RGB: // with or without alpha |
| // without alpha we just accept the JFIF defaults |
| if (hasAlpha) { |
| wantJFIF = false; |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| wantJFIF = false; |
| willSubsample = false; |
| if (cs.equals(ColorSpace.getInstance(ColorSpace.CS_PYCC))) { |
| willSubsample = true; |
| wantAdobe = true; |
| componentIDs[0] = (byte) 'Y'; |
| componentIDs[1] = (byte) 'C'; |
| componentIDs[2] = (byte) 'c'; |
| if (hasAlpha) { |
| componentIDs[3] = (byte) 'A'; |
| } |
| } |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| if (hasExtraComponents) { // e.g. K or alpha |
| wantJFIF = false; |
| if (!hasAlpha) { // then it must be K |
| wantAdobe = true; |
| transform = JPEG.ADOBE_YCCK; |
| } |
| } |
| break; |
| case ColorSpace.TYPE_CMYK: |
| wantJFIF = false; |
| wantAdobe = true; |
| transform = JPEG.ADOBE_YCCK; |
| break; |
| |
| default: |
| // Everything else is not subsampled, gets no special marker, |
| // and component ids are 0 - N |
| wantJFIF = false; |
| willSubsample = false; |
| } |
| |
| } |
| |
| // do we want an ICC profile? |
| if (wantJFIF && JPEG.isNonStandardICC(cs)) { |
| wantICC = true; |
| } |
| |
| // Now step through the markers, consulting our variables. |
| if (wantJFIF) { |
| JFIFMarkerSegment jfif = new JFIFMarkerSegment(); |
| markerSequence.add(jfif); |
| if (wantICC) { |
| try { |
| jfif.addICC((ICC_ColorSpace)cs); |
| } catch (IOException e) {} // Can't happen here |
| } |
| } |
| // Adobe |
| if (wantAdobe) { |
| markerSequence.add(new AdobeMarkerSegment(transform)); |
| } |
| |
| // dqt |
| if (wantQTables) { |
| markerSequence.add(new DQTMarkerSegment(quality, willSubsample)); |
| } |
| |
| // dht |
| if (wantHTables) { |
| markerSequence.add(new DHTMarkerSegment(willSubsample)); |
| } |
| |
| // sof |
| markerSequence.add(new SOFMarkerSegment(wantProg, |
| wantExtended, |
| willSubsample, |
| componentIDs, |
| numComponents)); |
| |
| // sos |
| if (!wantProg) { // Default progression scans are done in the writer |
| markerSequence.add(new SOSMarkerSegment(willSubsample, |
| componentIDs, |
| numComponents)); |
| } |
| |
| // Defensive programming |
| if (!isConsistent()) { |
| throw new InternalError("Default image metadata is inconsistent"); |
| } |
| } |
| |
| ////// End of constructors |
| |
| // Utilities for dealing with the marker sequence. |
| // The first ones have package access for access from the writer. |
| |
| /** |
| * Returns the first MarkerSegment object in the list |
| * with the given tag, or null if none is found. |
| */ |
| MarkerSegment findMarkerSegment(int tag) { |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| if (seg.tag == tag) { |
| return seg; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the first or last MarkerSegment object in the list |
| * of the given class, or null if none is found. |
| */ |
| MarkerSegment findMarkerSegment(Class cls, boolean first) { |
| if (first) { |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| if (cls.isInstance(seg)) { |
| return seg; |
| } |
| } |
| } else { |
| ListIterator iter = markerSequence.listIterator(markerSequence.size()); |
| while (iter.hasPrevious()) { |
| MarkerSegment seg = (MarkerSegment)iter.previous(); |
| if (cls.isInstance(seg)) { |
| return seg; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the index of the first or last MarkerSegment in the list |
| * of the given class, or -1 if none is found. |
| */ |
| private int findMarkerSegmentPosition(Class cls, boolean first) { |
| if (first) { |
| ListIterator iter = markerSequence.listIterator(); |
| for (int i = 0; iter.hasNext(); i++) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| if (cls.isInstance(seg)) { |
| return i; |
| } |
| } |
| } else { |
| ListIterator iter = markerSequence.listIterator(markerSequence.size()); |
| for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) { |
| MarkerSegment seg = (MarkerSegment)iter.previous(); |
| if (cls.isInstance(seg)) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| } |
| |
| private int findLastUnknownMarkerSegmentPosition() { |
| ListIterator iter = markerSequence.listIterator(markerSequence.size()); |
| for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) { |
| MarkerSegment seg = (MarkerSegment)iter.previous(); |
| if (seg.unknown == true) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| // Implement Cloneable, but restrict access |
| |
| protected Object clone() { |
| JPEGMetadata newGuy = null; |
| try { |
| newGuy = (JPEGMetadata) super.clone(); |
| } catch (CloneNotSupportedException e) {} // won't happen |
| if (markerSequence != null) { |
| newGuy.markerSequence = (List) cloneSequence(); |
| } |
| newGuy.resetSequence = null; |
| return newGuy; |
| } |
| |
| /** |
| * Returns a deep copy of the current marker sequence. |
| */ |
| private List cloneSequence() { |
| if (markerSequence == null) { |
| return null; |
| } |
| List retval = new ArrayList(markerSequence.size()); |
| Iterator iter = markerSequence.iterator(); |
| while(iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| retval.add(seg.clone()); |
| } |
| |
| return retval; |
| } |
| |
| |
| // Tree methods |
| |
| public Node getAsTree(String formatName) { |
| if (formatName == null) { |
| throw new IllegalArgumentException("null formatName!"); |
| } |
| if (isStream) { |
| if (formatName.equals(JPEG.nativeStreamMetadataFormatName)) { |
| return getNativeTree(); |
| } |
| } else { |
| if (formatName.equals(JPEG.nativeImageMetadataFormatName)) { |
| return getNativeTree(); |
| } |
| if (formatName.equals |
| (IIOMetadataFormatImpl.standardMetadataFormatName)) { |
| return getStandardTree(); |
| } |
| } |
| throw new IllegalArgumentException("Unsupported format name: " |
| + formatName); |
| } |
| |
| IIOMetadataNode getNativeTree() { |
| IIOMetadataNode root; |
| IIOMetadataNode top; |
| Iterator iter = markerSequence.iterator(); |
| if (isStream) { |
| root = new IIOMetadataNode(JPEG.nativeStreamMetadataFormatName); |
| top = root; |
| } else { |
| IIOMetadataNode sequence = new IIOMetadataNode("markerSequence"); |
| if (!inThumb) { |
| root = new IIOMetadataNode(JPEG.nativeImageMetadataFormatName); |
| IIOMetadataNode header = new IIOMetadataNode("JPEGvariety"); |
| root.appendChild(header); |
| JFIFMarkerSegment jfif = (JFIFMarkerSegment) |
| findMarkerSegment(JFIFMarkerSegment.class, true); |
| if (jfif != null) { |
| iter.next(); // JFIF must be first, so this skips it |
| header.appendChild(jfif.getNativeNode()); |
| } |
| root.appendChild(sequence); |
| } else { |
| root = sequence; |
| } |
| top = sequence; |
| } |
| while(iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| top.appendChild(seg.getNativeNode()); |
| } |
| return root; |
| } |
| |
| // Standard tree node methods |
| |
| protected IIOMetadataNode getStandardChromaNode() { |
| hasAlpha = false; // Unless we find otherwise |
| |
| // Colorspace type - follow the rules in the spec |
| // First get the SOF marker segment, if there is one |
| SOFMarkerSegment sof = (SOFMarkerSegment) |
| findMarkerSegment(SOFMarkerSegment.class, true); |
| if (sof == null) { |
| // No image, so no chroma |
| return null; |
| } |
| |
| IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); |
| IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); |
| chroma.appendChild(csType); |
| |
| // get the number of channels |
| int numChannels = sof.componentSpecs.length; |
| |
| IIOMetadataNode numChanNode = new IIOMetadataNode("NumChannels"); |
| chroma.appendChild(numChanNode); |
| numChanNode.setAttribute("value", Integer.toString(numChannels)); |
| |
| // is there a JFIF marker segment? |
| if (findMarkerSegment(JFIFMarkerSegment.class, true) != null) { |
| if (numChannels == 1) { |
| csType.setAttribute("name", "GRAY"); |
| } else { |
| csType.setAttribute("name", "YCbCr"); |
| } |
| return chroma; |
| } |
| |
| // How about an Adobe marker segment? |
| AdobeMarkerSegment adobe = |
| (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); |
| if (adobe != null){ |
| switch (adobe.transform) { |
| case JPEG.ADOBE_YCCK: |
| csType.setAttribute("name", "YCCK"); |
| break; |
| case JPEG.ADOBE_YCC: |
| csType.setAttribute("name", "YCbCr"); |
| break; |
| case JPEG.ADOBE_UNKNOWN: |
| if (numChannels == 3) { |
| csType.setAttribute("name", "RGB"); |
| } else if (numChannels == 4) { |
| csType.setAttribute("name", "CMYK"); |
| } |
| break; |
| } |
| return chroma; |
| } |
| |
| // Neither marker. Check components |
| if (numChannels < 3) { |
| csType.setAttribute("name", "GRAY"); |
| if (numChannels == 2) { |
| hasAlpha = true; |
| } |
| return chroma; |
| } |
| |
| boolean idsAreJFIF = true; |
| |
| for (int i = 0; i < sof.componentSpecs.length; i++) { |
| int id = sof.componentSpecs[i].componentId; |
| if ((id < 1) || (id >= sof.componentSpecs.length)) { |
| idsAreJFIF = false; |
| } |
| } |
| |
| if (idsAreJFIF) { |
| csType.setAttribute("name", "YCbCr"); |
| if (numChannels == 4) { |
| hasAlpha = true; |
| } |
| return chroma; |
| } |
| |
| // Check against the letters |
| if ((sof.componentSpecs[0].componentId == 'R') |
| && (sof.componentSpecs[1].componentId == 'G') |
| && (sof.componentSpecs[2].componentId == 'B')){ |
| |
| csType.setAttribute("name", "RGB"); |
| if ((numChannels == 4) |
| && (sof.componentSpecs[3].componentId == 'A')) { |
| hasAlpha = true; |
| } |
| return chroma; |
| } |
| |
| if ((sof.componentSpecs[0].componentId == 'Y') |
| && (sof.componentSpecs[1].componentId == 'C') |
| && (sof.componentSpecs[2].componentId == 'c')){ |
| |
| csType.setAttribute("name", "PhotoYCC"); |
| if ((numChannels == 4) |
| && (sof.componentSpecs[3].componentId == 'A')) { |
| hasAlpha = true; |
| } |
| return chroma; |
| } |
| |
| // Finally, 3-channel subsampled are YCbCr, unsubsampled are RGB |
| // 4-channel subsampled are YCbCrA, unsubsampled are CMYK |
| |
| boolean subsampled = false; |
| |
| int hfactor = sof.componentSpecs[0].HsamplingFactor; |
| int vfactor = sof.componentSpecs[0].VsamplingFactor; |
| |
| for (int i = 1; i<sof.componentSpecs.length; i++) { |
| if ((sof.componentSpecs[i].HsamplingFactor != hfactor) |
| || (sof.componentSpecs[i].VsamplingFactor != vfactor)){ |
| subsampled = true; |
| break; |
| } |
| } |
| |
| if (subsampled) { |
| csType.setAttribute("name", "YCbCr"); |
| if (numChannels == 4) { |
| hasAlpha = true; |
| } |
| return chroma; |
| } |
| |
| // Not subsampled. numChannels < 3 is taken care of above |
| if (numChannels == 3) { |
| csType.setAttribute("name", "RGB"); |
| } else { |
| csType.setAttribute("name", "CMYK"); |
| } |
| |
| return chroma; |
| } |
| |
| protected IIOMetadataNode getStandardCompressionNode() { |
| |
| IIOMetadataNode compression = new IIOMetadataNode("Compression"); |
| |
| // CompressionTypeName |
| IIOMetadataNode name = new IIOMetadataNode("CompressionTypeName"); |
| name.setAttribute("value", "JPEG"); |
| compression.appendChild(name); |
| |
| // Lossless - false |
| IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); |
| lossless.setAttribute("value", "FALSE"); |
| compression.appendChild(lossless); |
| |
| // NumProgressiveScans - count sos segments |
| int sosCount = 0; |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment ms = (MarkerSegment) iter.next(); |
| if (ms.tag == JPEG.SOS) { |
| sosCount++; |
| } |
| } |
| if (sosCount != 0) { |
| IIOMetadataNode prog = new IIOMetadataNode("NumProgressiveScans"); |
| prog.setAttribute("value", Integer.toString(sosCount)); |
| compression.appendChild(prog); |
| } |
| |
| return compression; |
| } |
| |
| protected IIOMetadataNode getStandardDimensionNode() { |
| // If we have a JFIF marker segment, we know a little |
| // otherwise all we know is the orientation, which is always normal |
| IIOMetadataNode dim = new IIOMetadataNode("Dimension"); |
| IIOMetadataNode orient = new IIOMetadataNode("ImageOrientation"); |
| orient.setAttribute("value", "normal"); |
| dim.appendChild(orient); |
| |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); |
| if (jfif != null) { |
| |
| // Aspect Ratio is width of pixel / height of pixel |
| float aspectRatio; |
| if (jfif.resUnits == 0) { |
| // In this case they just encode aspect ratio directly |
| aspectRatio = ((float) jfif.Xdensity)/jfif.Ydensity; |
| } else { |
| // They are true densities (e.g. dpi) and must be inverted |
| aspectRatio = ((float) jfif.Ydensity)/jfif.Xdensity; |
| } |
| IIOMetadataNode aspect = new IIOMetadataNode("PixelAspectRatio"); |
| aspect.setAttribute("value", Float.toString(aspectRatio)); |
| dim.insertBefore(aspect, orient); |
| |
| // Pixel size |
| if (jfif.resUnits != 0) { |
| // 1 == dpi, 2 == dpc |
| float scale = (jfif.resUnits == 1) ? 25.4F : 10.0F; |
| |
| IIOMetadataNode horiz = |
| new IIOMetadataNode("HorizontalPixelSize"); |
| horiz.setAttribute("value", |
| Float.toString(scale/jfif.Xdensity)); |
| dim.appendChild(horiz); |
| |
| IIOMetadataNode vert = |
| new IIOMetadataNode("VerticalPixelSize"); |
| vert.setAttribute("value", |
| Float.toString(scale/jfif.Ydensity)); |
| dim.appendChild(vert); |
| } |
| } |
| return dim; |
| } |
| |
| protected IIOMetadataNode getStandardTextNode() { |
| IIOMetadataNode text = null; |
| // Add a text entry for each COM Marker Segment |
| if (findMarkerSegment(JPEG.COM) != null) { |
| text = new IIOMetadataNode("Text"); |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg.tag == JPEG.COM) { |
| COMMarkerSegment com = (COMMarkerSegment) seg; |
| IIOMetadataNode entry = new IIOMetadataNode("TextEntry"); |
| entry.setAttribute("keyword", "comment"); |
| entry.setAttribute("value", com.getComment()); |
| text.appendChild(entry); |
| } |
| } |
| } |
| return text; |
| } |
| |
| protected IIOMetadataNode getStandardTransparencyNode() { |
| IIOMetadataNode trans = null; |
| if (hasAlpha == true) { |
| trans = new IIOMetadataNode("Transparency"); |
| IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); |
| alpha.setAttribute("value", "nonpremultiplied"); // Always assume |
| trans.appendChild(alpha); |
| } |
| return trans; |
| } |
| |
| // Editing |
| |
| public boolean isReadOnly() { |
| return false; |
| } |
| |
| public void mergeTree(String formatName, Node root) |
| throws IIOInvalidTreeException { |
| if (formatName == null) { |
| throw new IllegalArgumentException("null formatName!"); |
| } |
| if (root == null) { |
| throw new IllegalArgumentException("null root!"); |
| } |
| List copy = null; |
| if (resetSequence == null) { |
| resetSequence = cloneSequence(); // Deep copy |
| copy = resetSequence; // Avoid cloning twice |
| } else { |
| copy = cloneSequence(); |
| } |
| if (isStream && |
| (formatName.equals(JPEG.nativeStreamMetadataFormatName))) { |
| mergeNativeTree(root); |
| } else if (!isStream && |
| (formatName.equals(JPEG.nativeImageMetadataFormatName))) { |
| mergeNativeTree(root); |
| } else if (!isStream && |
| (formatName.equals |
| (IIOMetadataFormatImpl.standardMetadataFormatName))) { |
| mergeStandardTree(root); |
| } else { |
| throw new IllegalArgumentException("Unsupported format name: " |
| + formatName); |
| } |
| if (!isConsistent()) { |
| markerSequence = copy; |
| throw new IIOInvalidTreeException |
| ("Merged tree is invalid; original restored", root); |
| } |
| } |
| |
| private void mergeNativeTree(Node root) throws IIOInvalidTreeException { |
| String name = root.getNodeName(); |
| if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName |
| : JPEG.nativeImageMetadataFormatName)) { |
| throw new IIOInvalidTreeException("Invalid root node name: " + name, |
| root); |
| } |
| if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence |
| throw new IIOInvalidTreeException( |
| "JPEGvariety and markerSequence nodes must be present", root); |
| } |
| mergeJFIFsubtree(root.getFirstChild()); |
| mergeSequenceSubtree(root.getLastChild()); |
| } |
| |
| /** |
| * Merge a JFIF subtree into the marker sequence, if the subtree |
| * is non-empty. |
| * If a JFIF marker exists, update it from the subtree. |
| * If none exists, create one from the subtree and insert it at the |
| * beginning of the marker sequence. |
| */ |
| private void mergeJFIFsubtree(Node JPEGvariety) |
| throws IIOInvalidTreeException { |
| if (JPEGvariety.getChildNodes().getLength() != 0) { |
| Node jfifNode = JPEGvariety.getFirstChild(); |
| // is there already a jfif marker segment? |
| JFIFMarkerSegment jfifSeg = |
| (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); |
| if (jfifSeg != null) { |
| jfifSeg.updateFromNativeNode(jfifNode, false); |
| } else { |
| // Add it as the first element in the list. |
| markerSequence.add(0, new JFIFMarkerSegment(jfifNode)); |
| } |
| } |
| } |
| |
| private void mergeSequenceSubtree(Node sequenceTree) |
| throws IIOInvalidTreeException { |
| NodeList children = sequenceTree.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node node = children.item(i); |
| String name = node.getNodeName(); |
| if (name.equals("dqt")) { |
| mergeDQTNode(node); |
| } else if (name.equals("dht")) { |
| mergeDHTNode(node); |
| } else if (name.equals("dri")) { |
| mergeDRINode(node); |
| } else if (name.equals("com")) { |
| mergeCOMNode(node); |
| } else if (name.equals("app14Adobe")) { |
| mergeAdobeNode(node); |
| } else if (name.equals("unknown")) { |
| mergeUnknownNode(node); |
| } else if (name.equals("sof")) { |
| mergeSOFNode(node); |
| } else if (name.equals("sos")) { |
| mergeSOSNode(node); |
| } else { |
| throw new IIOInvalidTreeException("Invalid node: " + name, node); |
| } |
| } |
| } |
| |
| /** |
| * Merge the given DQT node into the marker sequence. If there already |
| * exist DQT marker segments in the sequence, then each table in the |
| * node replaces the first table, in any DQT segment, with the same |
| * table id. If none of the existing DQT segments contain a table with |
| * the same id, then the table is added to the last existing DQT segment. |
| * If there are no DQT segments, then a new one is created and added |
| * as follows: |
| * If there are DHT segments, the new DQT segment is inserted before the |
| * first one. |
| * If there are no DHT segments, the new DQT segment is inserted before |
| * an SOF segment, if there is one. |
| * If there is no SOF segment, the new DQT segment is inserted before |
| * the first SOS segment, if there is one. |
| * If there is no SOS segment, the new DQT segment is added to the end |
| * of the sequence. |
| */ |
| private void mergeDQTNode(Node node) throws IIOInvalidTreeException { |
| // First collect any existing DQT nodes into a local list |
| ArrayList oldDQTs = new ArrayList(); |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DQTMarkerSegment) { |
| oldDQTs.add(seg); |
| } |
| } |
| if (!oldDQTs.isEmpty()) { |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node child = children.item(i); |
| int childID = MarkerSegment.getAttributeValue(child, |
| null, |
| "qtableId", |
| 0, 3, |
| true); |
| DQTMarkerSegment dqt = null; |
| int tableIndex = -1; |
| for (int j = 0; j < oldDQTs.size(); j++) { |
| DQTMarkerSegment testDQT = (DQTMarkerSegment) oldDQTs.get(j); |
| for (int k = 0; k < testDQT.tables.size(); k++) { |
| DQTMarkerSegment.Qtable testTable = |
| (DQTMarkerSegment.Qtable) testDQT.tables.get(k); |
| if (childID == testTable.tableID) { |
| dqt = testDQT; |
| tableIndex = k; |
| break; |
| } |
| } |
| if (dqt != null) break; |
| } |
| if (dqt != null) { |
| dqt.tables.set(tableIndex, dqt.getQtableFromNode(child)); |
| } else { |
| dqt = (DQTMarkerSegment) oldDQTs.get(oldDQTs.size()-1); |
| dqt.tables.add(dqt.getQtableFromNode(child)); |
| } |
| } |
| } else { |
| DQTMarkerSegment newGuy = new DQTMarkerSegment(node); |
| int firstDHT = findMarkerSegmentPosition(DHTMarkerSegment.class, true); |
| int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); |
| int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); |
| if (firstDHT != -1) { |
| markerSequence.add(firstDHT, newGuy); |
| } else if (firstSOF != -1) { |
| markerSequence.add(firstSOF, newGuy); |
| } else if (firstSOS != -1) { |
| markerSequence.add(firstSOS, newGuy); |
| } else { |
| markerSequence.add(newGuy); |
| } |
| } |
| } |
| |
| /** |
| * Merge the given DHT node into the marker sequence. If there already |
| * exist DHT marker segments in the sequence, then each table in the |
| * node replaces the first table, in any DHT segment, with the same |
| * table class and table id. If none of the existing DHT segments contain |
| * a table with the same class and id, then the table is added to the last |
| * existing DHT segment. |
| * If there are no DHT segments, then a new one is created and added |
| * as follows: |
| * If there are DQT segments, the new DHT segment is inserted immediately |
| * following the last DQT segment. |
| * If there are no DQT segments, the new DHT segment is inserted before |
| * an SOF segment, if there is one. |
| * If there is no SOF segment, the new DHT segment is inserted before |
| * the first SOS segment, if there is one. |
| * If there is no SOS segment, the new DHT segment is added to the end |
| * of the sequence. |
| */ |
| private void mergeDHTNode(Node node) throws IIOInvalidTreeException { |
| // First collect any existing DQT nodes into a local list |
| ArrayList oldDHTs = new ArrayList(); |
| Iterator iter = markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DHTMarkerSegment) { |
| oldDHTs.add(seg); |
| } |
| } |
| if (!oldDHTs.isEmpty()) { |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node child = children.item(i); |
| NamedNodeMap attrs = child.getAttributes(); |
| int childID = MarkerSegment.getAttributeValue(child, |
| attrs, |
| "htableId", |
| 0, 3, |
| true); |
| int childClass = MarkerSegment.getAttributeValue(child, |
| attrs, |
| "class", |
| 0, 1, |
| true); |
| DHTMarkerSegment dht = null; |
| int tableIndex = -1; |
| for (int j = 0; j < oldDHTs.size(); j++) { |
| DHTMarkerSegment testDHT = (DHTMarkerSegment) oldDHTs.get(j); |
| for (int k = 0; k < testDHT.tables.size(); k++) { |
| DHTMarkerSegment.Htable testTable = |
| (DHTMarkerSegment.Htable) testDHT.tables.get(k); |
| if ((childID == testTable.tableID) && |
| (childClass == testTable.tableClass)) { |
| dht = testDHT; |
| tableIndex = k; |
| break; |
| } |
| } |
| if (dht != null) break; |
| } |
| if (dht != null) { |
| dht.tables.set(tableIndex, dht.getHtableFromNode(child)); |
| } else { |
| dht = (DHTMarkerSegment) oldDHTs.get(oldDHTs.size()-1); |
| dht.tables.add(dht.getHtableFromNode(child)); |
| } |
| } |
| } else { |
| DHTMarkerSegment newGuy = new DHTMarkerSegment(node); |
| int lastDQT = findMarkerSegmentPosition(DQTMarkerSegment.class, false); |
| int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); |
| int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); |
| if (lastDQT != -1) { |
| markerSequence.add(lastDQT+1, newGuy); |
| } else if (firstSOF != -1) { |
| markerSequence.add(firstSOF, newGuy); |
| } else if (firstSOS != -1) { |
| markerSequence.add(firstSOS, newGuy); |
| } else { |
| markerSequence.add(newGuy); |
| } |
| } |
| } |
| |
| /** |
| * Merge the given DRI node into the marker sequence. |
| * If there already exists a DRI marker segment, the restart interval |
| * value is updated. |
| * If there is no DRI segment, then a new one is created and added as |
| * follows: |
| * If there is an SOF segment, the new DRI segment is inserted before |
| * it. |
| * If there is no SOF segment, the new DRI segment is inserted before |
| * the first SOS segment, if there is one. |
| * If there is no SOS segment, the new DRI segment is added to the end |
| * of the sequence. |
| */ |
| private void mergeDRINode(Node node) throws IIOInvalidTreeException { |
| DRIMarkerSegment dri = |
| (DRIMarkerSegment) findMarkerSegment(DRIMarkerSegment.class, true); |
| if (dri != null) { |
| dri.updateFromNativeNode(node, false); |
| } else { |
| DRIMarkerSegment newGuy = new DRIMarkerSegment(node); |
| int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true); |
| int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); |
| if (firstSOF != -1) { |
| markerSequence.add(firstSOF, newGuy); |
| } else if (firstSOS != -1) { |
| markerSequence.add(firstSOS, newGuy); |
| } else { |
| markerSequence.add(newGuy); |
| } |
| } |
| } |
| |
| /** |
| * Merge the given COM node into the marker sequence. |
| * A new COM marker segment is created and added to the sequence |
| * using insertCOMMarkerSegment. |
| */ |
| private void mergeCOMNode(Node node) throws IIOInvalidTreeException { |
| COMMarkerSegment newGuy = new COMMarkerSegment(node); |
| insertCOMMarkerSegment(newGuy); |
| } |
| |
| /** |
| * Insert a new COM marker segment into an appropriate place in the |
| * marker sequence, as follows: |
| * If there already exist COM marker segments, the new one is inserted |
| * after the last one. |
| * If there are no COM segments, the new COM segment is inserted after the |
| * JFIF segment, if there is one. |
| * If there is no JFIF segment, the new COM segment is inserted after the |
| * Adobe marker segment, if there is one. |
| * If there is no Adobe segment, the new COM segment is inserted |
| * at the beginning of the sequence. |
| */ |
| private void insertCOMMarkerSegment(COMMarkerSegment newGuy) { |
| int lastCOM = findMarkerSegmentPosition(COMMarkerSegment.class, false); |
| boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null); |
| int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true); |
| if (lastCOM != -1) { |
| markerSequence.add(lastCOM+1, newGuy); |
| } else if (hasJFIF) { |
| markerSequence.add(1, newGuy); // JFIF is always 0 |
| } else if (firstAdobe != -1) { |
| markerSequence.add(firstAdobe+1, newGuy); |
| } else { |
| markerSequence.add(0, newGuy); |
| } |
| } |
| |
| /** |
| * Merge the given Adobe APP14 node into the marker sequence. |
| * If there already exists an Adobe marker segment, then its attributes |
| * are updated from the node. |
| * If there is no Adobe segment, then a new one is created and added |
| * using insertAdobeMarkerSegment. |
| */ |
| private void mergeAdobeNode(Node node) throws IIOInvalidTreeException { |
| AdobeMarkerSegment adobe = |
| (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); |
| if (adobe != null) { |
| adobe.updateFromNativeNode(node, false); |
| } else { |
| AdobeMarkerSegment newGuy = new AdobeMarkerSegment(node); |
| insertAdobeMarkerSegment(newGuy); |
| } |
| } |
| |
| /** |
| * Insert the given AdobeMarkerSegment into the marker sequence, as |
| * follows (we assume there is no Adobe segment yet): |
| * If there is a JFIF segment, then the new Adobe segment is inserted |
| * after it. |
| * If there is no JFIF segment, the new Adobe segment is inserted after the |
| * last Unknown segment, if there are any. |
| * If there are no Unknown segments, the new Adobe segment is inserted |
| * at the beginning of the sequence. |
| */ |
| private void insertAdobeMarkerSegment(AdobeMarkerSegment newGuy) { |
| boolean hasJFIF = |
| (findMarkerSegment(JFIFMarkerSegment.class, true) != null); |
| int lastUnknown = findLastUnknownMarkerSegmentPosition(); |
| if (hasJFIF) { |
| markerSequence.add(1, newGuy); // JFIF is always 0 |
| } else if (lastUnknown != -1) { |
| markerSequence.add(lastUnknown+1, newGuy); |
| } else { |
| markerSequence.add(0, newGuy); |
| } |
| } |
| |
| /** |
| * Merge the given Unknown node into the marker sequence. |
| * A new Unknown marker segment is created and added to the sequence as |
| * follows: |
| * If there already exist Unknown marker segments, the new one is inserted |
| * after the last one. |
| * If there are no Unknown marker segments, the new Unknown marker segment |
| * is inserted after the JFIF segment, if there is one. |
| * If there is no JFIF segment, the new Unknown segment is inserted before |
| * the Adobe marker segment, if there is one. |
| * If there is no Adobe segment, the new Unknown segment is inserted |
| * at the beginning of the sequence. |
| */ |
| private void mergeUnknownNode(Node node) throws IIOInvalidTreeException { |
| MarkerSegment newGuy = new MarkerSegment(node); |
| int lastUnknown = findLastUnknownMarkerSegmentPosition(); |
| boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null); |
| int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true); |
| if (lastUnknown != -1) { |
| markerSequence.add(lastUnknown+1, newGuy); |
| } else if (hasJFIF) { |
| markerSequence.add(1, newGuy); // JFIF is always 0 |
| } if (firstAdobe != -1) { |
| markerSequence.add(firstAdobe, newGuy); |
| } else { |
| markerSequence.add(0, newGuy); |
| } |
| } |
| |
| /** |
| * Merge the given SOF node into the marker sequence. |
| * If there already exists an SOF marker segment in the sequence, then |
| * its values are updated from the node. |
| * If there is no SOF segment, then a new one is created and added as |
| * follows: |
| * If there are any SOS segments, the new SOF segment is inserted before |
| * the first one. |
| * If there is no SOS segment, the new SOF segment is added to the end |
| * of the sequence. |
| * |
| */ |
| private void mergeSOFNode(Node node) throws IIOInvalidTreeException { |
| SOFMarkerSegment sof = |
| (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); |
| if (sof != null) { |
| sof.updateFromNativeNode(node, false); |
| } else { |
| SOFMarkerSegment newGuy = new SOFMarkerSegment(node); |
| int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true); |
| if (firstSOS != -1) { |
| markerSequence.add(firstSOS, newGuy); |
| } else { |
| markerSequence.add(newGuy); |
| } |
| } |
| } |
| |
| /** |
| * Merge the given SOS node into the marker sequence. |
| * If there already exists a single SOS marker segment, then the values |
| * are updated from the node. |
| * If there are more than one existing SOS marker segments, then an |
| * IIOInvalidTreeException is thrown, as SOS segments cannot be merged |
| * into a set of progressive scans. |
| * If there are no SOS marker segments, a new one is created and added |
| * to the end of the sequence. |
| */ |
| private void mergeSOSNode(Node node) throws IIOInvalidTreeException { |
| SOSMarkerSegment firstSOS = |
| (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true); |
| SOSMarkerSegment lastSOS = |
| (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, false); |
| if (firstSOS != null) { |
| if (firstSOS != lastSOS) { |
| throw new IIOInvalidTreeException |
| ("Can't merge SOS node into a tree with > 1 SOS node", node); |
| } |
| firstSOS.updateFromNativeNode(node, false); |
| } else { |
| markerSequence.add(new SOSMarkerSegment(node)); |
| } |
| } |
| |
| private boolean transparencyDone; |
| |
| private void mergeStandardTree(Node root) throws IIOInvalidTreeException { |
| transparencyDone = false; |
| NodeList children = root.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node node = children.item(i); |
| String name = node.getNodeName(); |
| if (name.equals("Chroma")) { |
| mergeStandardChromaNode(node, children); |
| } else if (name.equals("Compression")) { |
| mergeStandardCompressionNode(node); |
| } else if (name.equals("Data")) { |
| mergeStandardDataNode(node); |
| } else if (name.equals("Dimension")) { |
| mergeStandardDimensionNode(node); |
| } else if (name.equals("Document")) { |
| mergeStandardDocumentNode(node); |
| } else if (name.equals("Text")) { |
| mergeStandardTextNode(node); |
| } else if (name.equals("Transparency")) { |
| mergeStandardTransparencyNode(node); |
| } else { |
| throw new IIOInvalidTreeException("Invalid node: " + name, node); |
| } |
| } |
| } |
| |
| /* |
| * In general, it could be possible to convert all non-pixel data to some |
| * textual form and include it in comments, but then this would create the |
| * expectation that these comment forms be recognized by the reader, thus |
| * creating a defacto extension to JPEG metadata capabilities. This is |
| * probably best avoided, so the following convert only text nodes to |
| * comments, and lose the keywords as well. |
| */ |
| |
| private void mergeStandardChromaNode(Node node, NodeList siblings) |
| throws IIOInvalidTreeException { |
| // ColorSpaceType can change the target colorspace for compression |
| // This must take any transparency node into account as well, as |
| // that affects the number of channels (if alpha is present). If |
| // a transparency node is dealt with here, set a flag to indicate |
| // this to the transparency processor below. If we discover that |
| // the nodes are not in order, throw an exception as the tree is |
| // invalid. |
| |
| if (transparencyDone) { |
| throw new IIOInvalidTreeException |
| ("Transparency node must follow Chroma node", node); |
| } |
| |
| Node csType = node.getFirstChild(); |
| if ((csType == null) || !csType.getNodeName().equals("ColorSpaceType")) { |
| // If there is no ColorSpaceType node, we have nothing to do |
| return; |
| } |
| |
| String csName = csType.getAttributes().getNamedItem("name").getNodeValue(); |
| |
| int numChannels = 0; |
| boolean wantJFIF = false; |
| boolean wantAdobe = false; |
| int transform = 0; |
| boolean willSubsample = false; |
| byte [] ids = {1, 2, 3, 4}; // JFIF compatible |
| if (csName.equals("GRAY")) { |
| numChannels = 1; |
| wantJFIF = true; |
| } else if (csName.equals("YCbCr")) { |
| numChannels = 3; |
| wantJFIF = true; |
| willSubsample = true; |
| } else if (csName.equals("PhotoYCC")) { |
| numChannels = 3; |
| wantAdobe = true; |
| transform = JPEG.ADOBE_YCC; |
| ids[0] = (byte) 'Y'; |
| ids[1] = (byte) 'C'; |
| ids[2] = (byte) 'c'; |
| } else if (csName.equals("RGB")) { |
| numChannels = 3; |
| wantAdobe = true; |
| transform = JPEG.ADOBE_UNKNOWN; |
| ids[0] = (byte) 'R'; |
| ids[1] = (byte) 'G'; |
| ids[2] = (byte) 'B'; |
| } else if ((csName.equals("XYZ")) |
| || (csName.equals("Lab")) |
| || (csName.equals("Luv")) |
| || (csName.equals("YxY")) |
| || (csName.equals("HSV")) |
| || (csName.equals("HLS")) |
| || (csName.equals("CMY")) |
| || (csName.equals("3CLR"))) { |
| numChannels = 3; |
| } else if (csName.equals("YCCK")) { |
| numChannels = 4; |
| wantAdobe = true; |
| transform = JPEG.ADOBE_YCCK; |
| willSubsample = true; |
| } else if (csName.equals("CMYK")) { |
| numChannels = 4; |
| wantAdobe = true; |
| transform = JPEG.ADOBE_UNKNOWN; |
| } else if (csName.equals("4CLR")) { |
| numChannels = 4; |
| } else { // We can't handle them, so don't modify any metadata |
| return; |
| } |
| |
| boolean wantAlpha = false; |
| for (int i = 0; i < siblings.getLength(); i++) { |
| Node trans = siblings.item(i); |
| if (trans.getNodeName().equals("Transparency")) { |
| wantAlpha = wantAlpha(trans); |
| break; // out of for |
| } |
| } |
| |
| if (wantAlpha) { |
| numChannels++; |
| wantJFIF = false; |
| if (ids[0] == (byte) 'R') { |
| ids[3] = (byte) 'A'; |
| wantAdobe = false; |
| } |
| } |
| |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); |
| AdobeMarkerSegment adobe = |
| (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true); |
| SOFMarkerSegment sof = |
| (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); |
| SOSMarkerSegment sos = |
| (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true); |
| |
| // If the metadata specifies progressive, then the number of channels |
| // must match, so that we can modify all the existing SOS marker segments. |
| // If they don't match, we don't know what to do with SOS so we can't do |
| // the merge. We then just return silently. |
| // An exception would not be appropriate. A warning might, but we have |
| // nowhere to send it to. |
| if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive |
| if ((sof.componentSpecs.length != numChannels) && (sos != null)) { |
| return; |
| } |
| } |
| |
| // JFIF header might be removed |
| if (!wantJFIF && (jfif != null)) { |
| markerSequence.remove(jfif); |
| } |
| |
| // Now add a JFIF if we do want one, but only if it isn't stream metadata |
| if (wantJFIF && !isStream) { |
| markerSequence.add(0, new JFIFMarkerSegment()); |
| } |
| |
| // Adobe header might be removed or the transform modified, if it isn't |
| // stream metadata |
| if (wantAdobe) { |
| if ((adobe == null) && !isStream) { |
| adobe = new AdobeMarkerSegment(transform); |
| insertAdobeMarkerSegment(adobe); |
| } else { |
| adobe.transform = transform; |
| } |
| } else if (adobe != null) { |
| markerSequence.remove(adobe); |
| } |
| |
| boolean updateQtables = false; |
| boolean updateHtables = false; |
| |
| boolean progressive = false; |
| |
| int [] subsampledSelectors = {0, 1, 1, 0 } ; |
| int [] nonSubsampledSelectors = { 0, 0, 0, 0}; |
| |
| int [] newTableSelectors = willSubsample |
| ? subsampledSelectors |
| : nonSubsampledSelectors; |
| |
| // Keep the old componentSpecs array |
| SOFMarkerSegment.ComponentSpec [] oldCompSpecs = null; |
| // SOF might be modified |
| if (sof != null) { |
| oldCompSpecs = sof.componentSpecs; |
| progressive = (sof.tag == JPEG.SOF2); |
| // Now replace the SOF with a new one; it might be the same, but |
| // this is easier. |
| markerSequence.set(markerSequence.indexOf(sof), |
| new SOFMarkerSegment(progressive, |
| false, // we never need extended |
| willSubsample, |
| ids, |
| numChannels)); |
| |
| // Now suss out if subsampling changed and set the boolean for |
| // updating the q tables |
| // if the old componentSpec q table selectors don't match |
| // the new ones, update the qtables. The new selectors are already |
| // in place in the new SOF segment above. |
| for (int i = 0; i < oldCompSpecs.length; i++) { |
| if (oldCompSpecs[i].QtableSelector != newTableSelectors[i]) { |
| updateQtables = true; |
| } |
| } |
| |
| if (progressive) { |
| // if the component ids are different, update all the existing scans |
| // ignore Huffman tables |
| boolean idsDiffer = false; |
| for (int i = 0; i < oldCompSpecs.length; i++) { |
| if (ids[i] != oldCompSpecs[i].componentId) { |
| idsDiffer = true; |
| } |
| } |
| if (idsDiffer) { |
| // update the ids in each SOS marker segment |
| for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof SOSMarkerSegment) { |
| SOSMarkerSegment target = (SOSMarkerSegment) seg; |
| for (int i = 0; i < target.componentSpecs.length; i++) { |
| int oldSelector = |
| target.componentSpecs[i].componentSelector; |
| // Find the position in the old componentSpecs array |
| // of the old component with the old selector |
| // and replace the component selector with the |
| // new id at the same position, as these match |
| // the new component specs array in the SOF created |
| // above. |
| for (int j = 0; j < oldCompSpecs.length; j++) { |
| if (oldCompSpecs[j].componentId == oldSelector) { |
| target.componentSpecs[i].componentSelector = |
| ids[j]; |
| } |
| } |
| } |
| } |
| } |
| } |
| } else { |
| if (sos != null) { |
| // htables - if the old htable selectors don't match the new ones, |
| // update the tables. |
| for (int i = 0; i < sos.componentSpecs.length; i++) { |
| if ((sos.componentSpecs[i].dcHuffTable |
| != newTableSelectors[i]) |
| || (sos.componentSpecs[i].acHuffTable |
| != newTableSelectors[i])) { |
| updateHtables = true; |
| } |
| } |
| |
| // Might be the same as the old one, but this is easier. |
| markerSequence.set(markerSequence.indexOf(sos), |
| new SOSMarkerSegment(willSubsample, |
| ids, |
| numChannels)); |
| } |
| } |
| } else { |
| // should be stream metadata if there isn't an SOF, but check it anyway |
| if (isStream) { |
| // update tables - routines below check if it's really necessary |
| updateQtables = true; |
| updateHtables = true; |
| } |
| } |
| |
| if (updateQtables) { |
| List tableSegments = new ArrayList(); |
| for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DQTMarkerSegment) { |
| tableSegments.add(seg); |
| } |
| } |
| // If there are no tables, don't add them, as the metadata encodes an |
| // abbreviated stream. |
| // If we are not subsampling, we just need one, so don't do anything |
| if (!tableSegments.isEmpty() && willSubsample) { |
| // Is it really necessary? There should be at least 2 tables. |
| // If there is only one, assume it's a scaled "standard" |
| // luminance table, extract the scaling factor, and generate a |
| // scaled "standard" chrominance table. |
| |
| // Find the table with selector 1. |
| boolean found = false; |
| for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { |
| DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next(); |
| for (Iterator tabiter = testdqt.tables.iterator(); |
| tabiter.hasNext();) { |
| DQTMarkerSegment.Qtable tab = |
| (DQTMarkerSegment.Qtable) tabiter.next(); |
| if (tab.tableID == 1) { |
| found = true; |
| } |
| } |
| } |
| if (!found) { |
| // find the table with selector 0. There should be one. |
| DQTMarkerSegment.Qtable table0 = null; |
| for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { |
| DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next(); |
| for (Iterator tabiter = testdqt.tables.iterator(); |
| tabiter.hasNext();) { |
| DQTMarkerSegment.Qtable tab = |
| (DQTMarkerSegment.Qtable) tabiter.next(); |
| if (tab.tableID == 0) { |
| table0 = tab; |
| } |
| } |
| } |
| |
| // Assuming that the table with id 0 is a luminance table, |
| // compute a new chrominance table of the same quality and |
| // add it to the last DQT segment |
| DQTMarkerSegment dqt = |
| (DQTMarkerSegment) tableSegments.get(tableSegments.size()-1); |
| dqt.tables.add(dqt.getChromaForLuma(table0)); |
| } |
| } |
| } |
| |
| if (updateHtables) { |
| List tableSegments = new ArrayList(); |
| for (Iterator iter = markerSequence.iterator(); iter.hasNext();) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DHTMarkerSegment) { |
| tableSegments.add(seg); |
| } |
| } |
| // If there are no tables, don't add them, as the metadata encodes an |
| // abbreviated stream. |
| // If we are not subsampling, we just need one, so don't do anything |
| if (!tableSegments.isEmpty() && willSubsample) { |
| // Is it really necessary? There should be at least 2 dc and 2 ac |
| // tables. If there is only one, add a |
| // "standard " chrominance table. |
| |
| // find a table with selector 1. AC/DC is irrelevant |
| boolean found = false; |
| for (Iterator iter = tableSegments.iterator(); iter.hasNext();) { |
| DHTMarkerSegment testdht = (DHTMarkerSegment) iter.next(); |
| for (Iterator tabiter = testdht.tables.iterator(); |
| tabiter.hasNext();) { |
| DHTMarkerSegment.Htable tab = |
| (DHTMarkerSegment.Htable) tabiter.next(); |
| if (tab.tableID == 1) { |
| found = true; |
| } |
| } |
| } |
| if (!found) { |
| // Create new standard dc and ac chrominance tables and add them |
| // to the last DHT segment |
| DHTMarkerSegment lastDHT = |
| (DHTMarkerSegment) tableSegments.get(tableSegments.size()-1); |
| lastDHT.addHtable(JPEGHuffmanTable.StdDCLuminance, true, 1); |
| lastDHT.addHtable(JPEGHuffmanTable.StdACLuminance, true, 1); |
| } |
| } |
| } |
| } |
| |
| private boolean wantAlpha(Node transparency) { |
| boolean returnValue = false; |
| Node alpha = transparency.getFirstChild(); // Alpha must be first if present |
| if (alpha.getNodeName().equals("Alpha")) { |
| if (alpha.hasAttributes()) { |
| String value = |
| alpha.getAttributes().getNamedItem("value").getNodeValue(); |
| if (!value.equals("none")) { |
| returnValue = true; |
| } |
| } |
| } |
| transparencyDone = true; |
| return returnValue; |
| } |
| |
| private void mergeStandardCompressionNode(Node node) |
| throws IIOInvalidTreeException { |
| // NumProgressiveScans is ignored. Progression must be enabled on the |
| // ImageWriteParam. |
| // No-op |
| } |
| |
| private void mergeStandardDataNode(Node node) |
| throws IIOInvalidTreeException { |
| // No-op |
| } |
| |
| private void mergeStandardDimensionNode(Node node) |
| throws IIOInvalidTreeException { |
| // Pixel Aspect Ratio or pixel size can be incorporated if there is, |
| // or can be, a JFIF segment |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true); |
| if (jfif == null) { |
| // Can there be one? |
| // Criteria: |
| // SOF must be present with 1 or 3 channels, (stream metadata fails this) |
| // Component ids must be JFIF compatible. |
| boolean canHaveJFIF = false; |
| SOFMarkerSegment sof = |
| (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true); |
| if (sof != null) { |
| int numChannels = sof.componentSpecs.length; |
| if ((numChannels == 1) || (numChannels == 3)) { |
| canHaveJFIF = true; // remaining tests are negative |
| for (int i = 0; i < sof.componentSpecs.length; i++) { |
| if (sof.componentSpecs[i].componentId != i+1) |
| canHaveJFIF = false; |
| } |
| // if Adobe present, transform = ADOBE_UNKNOWN for 1-channel, |
| // ADOBE_YCC for 3-channel. |
| AdobeMarkerSegment adobe = |
| (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, |
| true); |
| if (adobe != null) { |
| if (adobe.transform != ((numChannels == 1) |
| ? JPEG.ADOBE_UNKNOWN |
| : JPEG.ADOBE_YCC)) { |
| canHaveJFIF = false; |
| } |
| } |
| } |
| } |
| // If so, create one and insert it into the sequence. Note that |
| // default is just pixel ratio at 1:1 |
| if (canHaveJFIF) { |
| jfif = new JFIFMarkerSegment(); |
| markerSequence.add(0, jfif); |
| } |
| } |
| if (jfif != null) { |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node child = children.item(i); |
| NamedNodeMap attrs = child.getAttributes(); |
| String name = child.getNodeName(); |
| if (name.equals("PixelAspectRatio")) { |
| String valueString = attrs.getNamedItem("value").getNodeValue(); |
| float value = Float.parseFloat(valueString); |
| Point p = findIntegerRatio(value); |
| jfif.resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO; |
| jfif.Xdensity = p.x; |
| jfif.Xdensity = p.y; |
| } else if (name.equals("HorizontalPixelSize")) { |
| String valueString = attrs.getNamedItem("value").getNodeValue(); |
| float value = Float.parseFloat(valueString); |
| // Convert from mm/dot to dots/cm |
| int dpcm = (int) Math.round(1.0/(value*10.0)); |
| jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM; |
| jfif.Xdensity = dpcm; |
| } else if (name.equals("VerticalPixelSize")) { |
| String valueString = attrs.getNamedItem("value").getNodeValue(); |
| float value = Float.parseFloat(valueString); |
| // Convert from mm/dot to dots/cm |
| int dpcm = (int) Math.round(1.0/(value*10.0)); |
| jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM; |
| jfif.Ydensity = dpcm; |
| } |
| |
| } |
| } |
| } |
| |
| /* |
| * Return a pair of integers whose ratio (x/y) approximates the given |
| * float value. |
| */ |
| private static Point findIntegerRatio(float value) { |
| float epsilon = 0.005F; |
| |
| // Normalize |
| value = Math.abs(value); |
| |
| // Deal with min case |
| if (value <= epsilon) { |
| return new Point(1, 255); |
| } |
| |
| // Deal with max case |
| if (value >= 255) { |
| return new Point(255, 1); |
| } |
| |
| // Remember if we invert |
| boolean inverted = false; |
| if (value < 1.0) { |
| value = 1.0F/value; |
| inverted = true; |
| } |
| |
| // First approximation |
| int y = 1; |
| int x = (int) Math.round(value); |
| |
| float ratio = (float) x; |
| float delta = Math.abs(value - ratio); |
| while (delta > epsilon) { // not close enough |
| // Increment y and compute a new x |
| y++; |
| x = (int) Math.round(y*value); |
| ratio = (float)x/(float)y; |
| delta = Math.abs(value - ratio); |
| } |
| return inverted ? new Point(y, x) : new Point(x, y); |
| } |
| |
| private void mergeStandardDocumentNode(Node node) |
| throws IIOInvalidTreeException { |
| // No-op |
| } |
| |
| private void mergeStandardTextNode(Node node) |
| throws IIOInvalidTreeException { |
| // Convert to comments. For the moment ignore the encoding issue. |
| // Ignore keywords, language, and encoding (for the moment). |
| // If compression tag is present, use only entries with "none". |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node child = children.item(i); |
| NamedNodeMap attrs = child.getAttributes(); |
| Node comp = attrs.getNamedItem("compression"); |
| boolean copyIt = true; |
| if (comp != null) { |
| String compString = comp.getNodeValue(); |
| if (!compString.equals("none")) { |
| copyIt = false; |
| } |
| } |
| if (copyIt) { |
| String value = attrs.getNamedItem("value").getNodeValue(); |
| COMMarkerSegment com = new COMMarkerSegment(value); |
| insertCOMMarkerSegment(com); |
| } |
| } |
| } |
| |
| private void mergeStandardTransparencyNode(Node node) |
| throws IIOInvalidTreeException { |
| // This might indicate that an alpha channel is being added or removed. |
| // The nodes must appear in order, and a Chroma node will process any |
| // transparency, so process it here only if there was no Chroma node |
| // Do nothing for stream metadata |
| if (!transparencyDone && !isStream) { |
| boolean wantAlpha = wantAlpha(node); |
| // do we have alpha already? If the number of channels is 2 or 4, |
| // we do, as we don't support CMYK, nor can we add alpha to it |
| // The number of channels can be determined from the SOF |
| JFIFMarkerSegment jfif = (JFIFMarkerSegment) findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| AdobeMarkerSegment adobe = (AdobeMarkerSegment) findMarkerSegment |
| (AdobeMarkerSegment.class, true); |
| SOFMarkerSegment sof = (SOFMarkerSegment) findMarkerSegment |
| (SOFMarkerSegment.class, true); |
| SOSMarkerSegment sos = (SOSMarkerSegment) findMarkerSegment |
| (SOSMarkerSegment.class, true); |
| |
| // We can do nothing for progressive, as we don't know how to |
| // modify the scans. |
| if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive |
| return; |
| } |
| |
| // Do we already have alpha? We can tell by the number of channels |
| // We must have an sof, or we can't do anything further |
| if (sof != null) { |
| int numChannels = sof.componentSpecs.length; |
| boolean hadAlpha = (numChannels == 2) || (numChannels == 4); |
| // proceed only if the old state and the new state differ |
| if (hadAlpha != wantAlpha) { |
| if (wantAlpha) { // Adding alpha |
| numChannels++; |
| if (jfif != null) { |
| markerSequence.remove(jfif); |
| } |
| |
| // If an adobe marker is present, transform must be UNKNOWN |
| if (adobe != null) { |
| adobe.transform = JPEG.ADOBE_UNKNOWN; |
| } |
| |
| // Add a component spec with appropriate parameters to SOF |
| SOFMarkerSegment.ComponentSpec [] newSpecs = |
| new SOFMarkerSegment.ComponentSpec[numChannels]; |
| for (int i = 0; i < sof.componentSpecs.length; i++) { |
| newSpecs[i] = sof.componentSpecs[i]; |
| } |
| byte oldFirstID = (byte) sof.componentSpecs[0].componentId; |
| byte newID = (byte) ((oldFirstID > 1) ? 'A' : 4); |
| newSpecs[numChannels-1] = |
| sof.getComponentSpec(newID, |
| sof.componentSpecs[0].HsamplingFactor, |
| sof.componentSpecs[0].QtableSelector); |
| |
| sof.componentSpecs = newSpecs; |
| |
| // Add a component spec with appropriate parameters to SOS |
| SOSMarkerSegment.ScanComponentSpec [] newScanSpecs = |
| new SOSMarkerSegment.ScanComponentSpec [numChannels]; |
| for (int i = 0; i < sos.componentSpecs.length; i++) { |
| newScanSpecs[i] = sos.componentSpecs[i]; |
| } |
| newScanSpecs[numChannels-1] = |
| sos.getScanComponentSpec (newID, 0); |
| sos.componentSpecs = newScanSpecs; |
| } else { // Removing alpha |
| numChannels--; |
| // Remove a component spec from SOF |
| SOFMarkerSegment.ComponentSpec [] newSpecs = |
| new SOFMarkerSegment.ComponentSpec[numChannels]; |
| for (int i = 0; i < numChannels; i++) { |
| newSpecs[i] = sof.componentSpecs[i]; |
| } |
| sof.componentSpecs = newSpecs; |
| |
| // Remove a component spec from SOS |
| SOSMarkerSegment.ScanComponentSpec [] newScanSpecs = |
| new SOSMarkerSegment.ScanComponentSpec [numChannels]; |
| for (int i = 0; i < numChannels; i++) { |
| newScanSpecs[i] = sos.componentSpecs[i]; |
| } |
| sos.componentSpecs = newScanSpecs; |
| } |
| } |
| } |
| } |
| } |
| |
| |
| public void setFromTree(String formatName, Node root) |
| throws IIOInvalidTreeException { |
| if (formatName == null) { |
| throw new IllegalArgumentException("null formatName!"); |
| } |
| if (root == null) { |
| throw new IllegalArgumentException("null root!"); |
| } |
| if (isStream && |
| (formatName.equals(JPEG.nativeStreamMetadataFormatName))) { |
| setFromNativeTree(root); |
| } else if (!isStream && |
| (formatName.equals(JPEG.nativeImageMetadataFormatName))) { |
| setFromNativeTree(root); |
| } else if (!isStream && |
| (formatName.equals |
| (IIOMetadataFormatImpl.standardMetadataFormatName))) { |
| // In this case a reset followed by a merge is correct |
| super.setFromTree(formatName, root); |
| } else { |
| throw new IllegalArgumentException("Unsupported format name: " |
| + formatName); |
| } |
| } |
| |
| private void setFromNativeTree(Node root) throws IIOInvalidTreeException { |
| if (resetSequence == null) { |
| resetSequence = markerSequence; |
| } |
| markerSequence = new ArrayList(); |
| |
| // Build a whole new marker sequence from the tree |
| |
| String name = root.getNodeName(); |
| if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName |
| : JPEG.nativeImageMetadataFormatName)) { |
| throw new IIOInvalidTreeException("Invalid root node name: " + name, |
| root); |
| } |
| if (!isStream) { |
| if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence |
| throw new IIOInvalidTreeException( |
| "JPEGvariety and markerSequence nodes must be present", root); |
| } |
| |
| Node JPEGvariety = root.getFirstChild(); |
| |
| if (JPEGvariety.getChildNodes().getLength() != 0) { |
| markerSequence.add(new JFIFMarkerSegment(JPEGvariety.getFirstChild())); |
| } |
| } |
| |
| Node markerSequenceNode = isStream ? root : root.getLastChild(); |
| setFromMarkerSequenceNode(markerSequenceNode); |
| |
| } |
| |
| void setFromMarkerSequenceNode(Node markerSequenceNode) |
| throws IIOInvalidTreeException{ |
| |
| NodeList children = markerSequenceNode.getChildNodes(); |
| // for all the children, add a marker segment |
| for (int i = 0; i < children.getLength(); i++) { |
| Node node = children.item(i); |
| String childName = node.getNodeName(); |
| if (childName.equals("dqt")) { |
| markerSequence.add(new DQTMarkerSegment(node)); |
| } else if (childName.equals("dht")) { |
| markerSequence.add(new DHTMarkerSegment(node)); |
| } else if (childName.equals("dri")) { |
| markerSequence.add(new DRIMarkerSegment(node)); |
| } else if (childName.equals("com")) { |
| markerSequence.add(new COMMarkerSegment(node)); |
| } else if (childName.equals("app14Adobe")) { |
| markerSequence.add(new AdobeMarkerSegment(node)); |
| } else if (childName.equals("unknown")) { |
| markerSequence.add(new MarkerSegment(node)); |
| } else if (childName.equals("sof")) { |
| markerSequence.add(new SOFMarkerSegment(node)); |
| } else if (childName.equals("sos")) { |
| markerSequence.add(new SOSMarkerSegment(node)); |
| } else { |
| throw new IIOInvalidTreeException("Invalid " |
| + (isStream ? "stream " : "image ") + "child: " |
| + childName, node); |
| } |
| } |
| } |
| |
| /** |
| * Check that this metadata object is in a consistent state and |
| * return <code>true</code> if it is or <code>false</code> |
| * otherwise. All the constructors and modifiers should call |
| * this method at the end to guarantee that the data is always |
| * consistent, as the writer relies on this. |
| */ |
| private boolean isConsistent() { |
| SOFMarkerSegment sof = |
| (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, |
| true); |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, |
| true); |
| AdobeMarkerSegment adobe = |
| (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, |
| true); |
| boolean retval = true; |
| if (!isStream) { |
| if (sof != null) { |
| // SOF numBands = total scan bands |
| int numSOFBands = sof.componentSpecs.length; |
| int numScanBands = countScanBands(); |
| if (numScanBands != 0) { // No SOS is OK |
| if (numScanBands != numSOFBands) { |
| retval = false; |
| } |
| } |
| // If JFIF is present, component ids are 1-3, bands are 1 or 3 |
| if (jfif != null) { |
| if ((numSOFBands != 1) && (numSOFBands != 3)) { |
| retval = false; |
| } |
| for (int i = 0; i < numSOFBands; i++) { |
| if (sof.componentSpecs[i].componentId != i+1) { |
| retval = false; |
| } |
| } |
| |
| // If both JFIF and Adobe are present, |
| // Adobe transform == unknown for gray, |
| // YCC for 3-chan. |
| if ((adobe != null) |
| && (((numSOFBands == 1) |
| && (adobe.transform != JPEG.ADOBE_UNKNOWN)) |
| || ((numSOFBands == 3) |
| && (adobe.transform != JPEG.ADOBE_YCC)))) { |
| retval = false; |
| } |
| } |
| } else { |
| // stream can't have jfif, adobe, sof, or sos |
| SOSMarkerSegment sos = |
| (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, |
| true); |
| if ((jfif != null) || (adobe != null) |
| || (sof != null) || (sos != null)) { |
| retval = false; |
| } |
| } |
| } |
| return retval; |
| } |
| |
| /** |
| * Returns the total number of bands referenced in all SOS marker |
| * segments, including 0 if there are no SOS marker segments. |
| */ |
| private int countScanBands() { |
| List ids = new ArrayList(); |
| Iterator iter = markerSequence.iterator(); |
| while(iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| if (seg instanceof SOSMarkerSegment) { |
| SOSMarkerSegment sos = (SOSMarkerSegment) seg; |
| SOSMarkerSegment.ScanComponentSpec [] specs = sos.componentSpecs; |
| for (int i = 0; i < specs.length; i++) { |
| Integer id = new Integer(specs[i].componentSelector); |
| if (!ids.contains(id)) { |
| ids.add(id); |
| } |
| } |
| } |
| } |
| |
| return ids.size(); |
| } |
| |
| ///// Writer support |
| |
| void writeToStream(ImageOutputStream ios, |
| boolean ignoreJFIF, |
| boolean forceJFIF, |
| List thumbnails, |
| ICC_Profile iccProfile, |
| boolean ignoreAdobe, |
| int newAdobeTransform, |
| JPEGImageWriter writer) |
| throws IOException { |
| if (forceJFIF) { |
| // Write a default JFIF segment, including thumbnails |
| // This won't be duplicated below because forceJFIF will be |
| // set only if there is no JFIF present already. |
| JFIFMarkerSegment.writeDefaultJFIF(ios, |
| thumbnails, |
| iccProfile, |
| writer); |
| if ((ignoreAdobe == false) |
| && (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE)) { |
| if ((newAdobeTransform != JPEG.ADOBE_UNKNOWN) |
| && (newAdobeTransform != JPEG.ADOBE_YCC)) { |
| // Not compatible, so ignore Adobe. |
| ignoreAdobe = true; |
| writer.warningOccurred |
| (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB); |
| } |
| } |
| } |
| // Iterate over each MarkerSegment |
| Iterator iter = markerSequence.iterator(); |
| while(iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment)iter.next(); |
| if (seg instanceof JFIFMarkerSegment) { |
| if (ignoreJFIF == false) { |
| JFIFMarkerSegment jfif = (JFIFMarkerSegment) seg; |
| jfif.writeWithThumbs(ios, thumbnails, writer); |
| if (iccProfile != null) { |
| JFIFMarkerSegment.writeICC(iccProfile, ios); |
| } |
| } // Otherwise ignore it, as requested |
| } else if (seg instanceof AdobeMarkerSegment) { |
| if (ignoreAdobe == false) { |
| if (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE) { |
| AdobeMarkerSegment newAdobe = |
| (AdobeMarkerSegment) seg.clone(); |
| newAdobe.transform = newAdobeTransform; |
| newAdobe.write(ios); |
| } else if (forceJFIF) { |
| // If adobe isn't JFIF compatible, ignore it |
| AdobeMarkerSegment adobe = (AdobeMarkerSegment) seg; |
| if ((adobe.transform == JPEG.ADOBE_UNKNOWN) |
| || (adobe.transform == JPEG.ADOBE_YCC)) { |
| adobe.write(ios); |
| } else { |
| writer.warningOccurred |
| (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB); |
| } |
| } else { |
| seg.write(ios); |
| } |
| } // Otherwise ignore it, as requested |
| } else { |
| seg.write(ios); |
| } |
| } |
| } |
| |
| //// End of writer support |
| |
| public void reset() { |
| if (resetSequence != null) { // Otherwise no need to reset |
| markerSequence = resetSequence; |
| resetSequence = null; |
| } |
| } |
| |
| public void print() { |
| for (int i = 0; i < markerSequence.size(); i++) { |
| MarkerSegment seg = (MarkerSegment) markerSequence.get(i); |
| seg.print(); |
| } |
| } |
| |
| } |