/* * Copyright (c) 2001, 2014, 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.BufferedImage; 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 markerSequence, created the first time the * markerSequence is modified. This is used by reset * to restore the original state. */ private List resetSequence = null; /** * Set to true 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 getAsTree. */ 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 MarkerSegment 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 JPEGMetadata object by reading the * contents of an ImageInputStream. 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 ImageInputStream from which to read * the metadata. * @param reader The JPEGImageReader 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) { newGuy = new MarkerSegment(buffer); newGuy.loadData(buffer); } else { 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 JPEGMetadata 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 JPEGMetadata 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 = 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 = iter.next(); if (cls.isInstance(seg)) { return seg; } } } else { ListIterator iter = markerSequence.listIterator(markerSequence.size()); while (iter.hasPrevious()) { MarkerSegment seg = 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 = 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 = 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 = 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 = 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 = iter.next(); retval.add((MarkerSegment) 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 = 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 = false; int cid0 = sof.componentSpecs[0].componentId; int cid1 = sof.componentSpecs[1].componentId; int cid2 = sof.componentSpecs[2].componentId; if ((cid0 == 1) && (cid1 == 2) && (cid2 == 3)) { idsAreJFIF = true; } 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 iter = markerSequence.iterator(); while (iter.hasNext()) { MarkerSegment ms = 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 = 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 = iter.next(); if (seg instanceof DQTMarkerSegment) { oldDQTs.add((DQTMarkerSegment) 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 = oldDQTs.get(j); for (int k = 0; k < testDQT.tables.size(); k++) { DQTMarkerSegment.Qtable testTable = 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 = 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 = iter.next(); if (seg instanceof DHTMarkerSegment) { oldDHTs.add((DHTMarkerSegment) 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 = oldDHTs.get(j); for (int k = 0; k < testDHT.tables.size(); k++) { DHTMarkerSegment.Htable testTable = 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 = 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 = 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 = iter.next(); if (seg instanceof DQTMarkerSegment) { tableSegments.add((DQTMarkerSegment) 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 = iter.next(); for (Iterator tabiter = testdqt.tables.iterator(); tabiter.hasNext();) { DQTMarkerSegment.Qtable tab = 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 = iter.next(); for (Iterator tabiter = testdqt.tables.iterator(); tabiter.hasNext();) { DQTMarkerSegment.Qtable tab = 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 = 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 = iter.next(); if (seg instanceof DHTMarkerSegment) { tableSegments.add((DHTMarkerSegment) 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 = iter.next(); for (Iterator tabiter = testdht.tables.iterator(); tabiter.hasNext();) { DHTMarkerSegment.Htable tab = 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 = 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 = 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 = 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 true if it is or false * 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 = 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 = 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 = 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 = markerSequence.get(i); seg.print(); } } }