2418 lines
95 KiB
2418 lines
95 KiB
/*
|
|
* Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
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) {
|
|
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 <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();
|
|
}
|
|
}
|
|
|
|
}
|