From ca48837e11edfef8c6c9b8d940df73adacfeffde Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 23 Oct 2013 14:56:01 +0200 Subject: [PATCH] TMI-JPEG-4: Moved metadata cleaning to separate class. Better class name welcome... ;-) --- .../jpeg/JPEGImage10MetadataCleaner.java | 270 ++++++++++++++++ .../imageio/plugins/jpeg/JPEGImageReader.java | 306 +++--------------- 2 files changed, 309 insertions(+), 267 deletions(-) create mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java new file mode 100644 index 00000000..791ffd13 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -0,0 +1,270 @@ +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.color.ICC_Profile; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +/** + * JPEGImage10MetadataCleaner + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImage10MetadataCleaner.java,v 1.0 22.10.13 14:41 haraldk Exp$ + */ +final class JPEGImage10MetadataCleaner { + + /** + * Native metadata format name + */ + static final String JAVAX_IMAGEIO_JPEG_IMAGE_1_0 = "javax_imageio_jpeg_image_1.0"; + + private final JPEGImageReader reader; + + JPEGImage10MetadataCleaner(JPEGImageReader reader) { + this.reader = reader; + } + + IIOMetadata cleanMetadata(final int imageIndex, final IIOMetadata imageMetadata, final List appSegments) throws IOException { + // We filter out pretty much everything from the stream.. + // Meaning we have to read get *all APP segments* and re-insert into metadata. + + // NOTE: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... + // DONE: 1: Work around + // TODO: 2: REPORT BUG! + // TODO: Report dht inconsistency bug (reads any amount of tables but only allows setting 4 tables) + + // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. Need new format, might as well create a completely new format... + // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: + // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata + /* + from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + + In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) + by defining other types of nodes which may appear as a child of the JPEGvariety node. + + (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the + javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an + APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific + code to interpret the data in the marker segment. If such an application were to encounter a metadata tree + formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be + unknown in that format - it might be structured as a child node of the JPEGvariety node. + + Thus, it is important for an application to specify which version to use by passing the string identifying + the version to the method/constructor used to obtain an IIOMetadata object.) + */ + + IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0); + IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0); + IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0); + + JFIFSegment jfifSegment = reader.getJFIF(); + JFXXSegment jfxxSegment = reader.getJFXX(); + AdobeDCTSegment adobeDCT = reader.getAdobeDCT(); + ICC_Profile embeddedICCProfile = reader.getEmbeddedICCProfile(true); + SOFSegment sof = reader.getSOF(); + + boolean hasRealJFIF = false; + boolean hasRealJFXX = false; + boolean hasRealICC = false; + + if (jfifSegment != null) { + // Normal case, conformant JFIF with 1 or 3 components + // TODO: Test if we have CMY or other non-JFIF color space? + if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) { + IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF"); + jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); + jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); + jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); + jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); + jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); + jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); + jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); + + jpegVariety.appendChild(jfif); + hasRealJFIF = true; + + // Add app2ICC and JFXX as proper nodes + if (embeddedICCProfile != null) { + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(embeddedICCProfile); + jfif.appendChild(app2ICC); + hasRealICC = true; + } + + if (jfxxSegment != null) { + IIOMetadataNode JFXX = new IIOMetadataNode("JFXX"); + jfif.appendChild(JFXX); + IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); + app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); + + JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), imageIndex, -1, jfxxSegment); + IIOMetadataNode jfifThumb; + + switch (jfxxSegment.extensionCode) { + case JFXXSegment.JPEG: + jfifThumb = new IIOMetadataNode("JFIFthumbJPEG"); + // Contains it's own "markerSequence" with full DHT, DQT, SOF etc... + IIOMetadata thumbMeta = thumbnailReader.readMetadata(); + Node thumbTree = thumbMeta.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0); + jfifThumb.appendChild(thumbTree.getLastChild()); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.INDEXED: + jfifThumb = new IIOMetadataNode("JFIFthumbPalette"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.RGB: + jfifThumb = new IIOMetadataNode("JFIFthumbRGB"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + default: + reader.processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode)); + } + + JFXX.appendChild(app0JFXX); + hasRealJFXX = true; + } + } + else { + // Typically CMYK JPEG with JFIF segment (Adobe or similar). + reader.processWarningOccurred(String.format( + "Incompatible JFIF marker segment in stream. " + + "SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); + } + } + + // Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF + if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) { + reader.processWarningOccurred(String.format( + "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + + "Ignoring Adobe App14 marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); + + // Remove bad AdobeDCT + NodeList app14Adobe = tree.getElementsByTagName("app14Adobe"); + for (int i = app14Adobe.getLength() - 1; i >= 0; i--) { + Node item = app14Adobe.item(i); + item.getParentNode().removeChild(item); + } + + // We don't add this as unknown marker, as we are certain it's bogus by now + } + + Node next = null; + for (JPEGSegment segment : appSegments) { + // Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers + if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) { + continue; + } + else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) { + continue; + } + else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) { + continue; + } + else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) { + continue; + } + else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) { + continue; + } + + IIOMetadataNode unknown = new IIOMetadataNode("unknown"); + unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff)); + + DataInputStream stream = new DataInputStream(segment.data()); + + try { + String identifier = segment.identifier(); + int off = identifier != null ? identifier.length() + 1 : 0; + + byte[] data = new byte[off + segment.length()]; + + if (identifier != null) { + System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length()); + } + + stream.readFully(data, off, segment.length()); + + unknown.setUserObject(data); + } + finally { + stream.close(); + } + + if (next == null) { + // To be semi-compatible with the functionality in mergeTree, + // let's insert after the last unknown tag, or before any other tag if no unknown tag exists + NodeList unknowns = markerSequence.getElementsByTagName("unknown"); + + if (unknowns.getLength() > 0) { + next = unknowns.item(unknowns.getLength() - 1).getNextSibling(); + } + else { + next = markerSequence.getFirstChild(); + } + } + + markerSequence.insertBefore(unknown, next); + } + + // Inconsistency issue in the com.sun classes, it can read metadata with dht containing + // more than 4 children, but will not allow setting such a tree... + // We'll split AC/DC tables into separate dht nodes. + NodeList dhts = markerSequence.getElementsByTagName("dht"); + for (int j = 0; j < dhts.getLength(); j++) { + Node dht = dhts.item(j); + NodeList dhtables = dht.getChildNodes(); + + if (dhtables.getLength() > 4) { + IIOMetadataNode acTables = new IIOMetadataNode("dht"); + dht.getParentNode().insertBefore(acTables, dht.getNextSibling()); + + // Split into 2 dht nodes, one for AC and one for DC + for (int i = 0; i < dhtables.getLength(); i++) { + Element dhtable = (Element) dhtables.item(i); + String tableClass = dhtable.getAttribute("class"); + if ("1".equals(tableClass)) { + dht.removeChild(dhtable); + acTables.appendChild(dhtable); + } + } + } + } + + try { + imageMetadata.setFromTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0, tree); + } + catch (IIOInvalidTreeException e) { + if (JPEGImageReader.DEBUG) { + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); + } + + throw e; + } + + return imageMetadata; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 210975cb..2940e5ff 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -42,16 +42,11 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.xml.XMLSerializer; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import javax.imageio.*; import javax.imageio.event.IIOReadUpdateListener; import javax.imageio.event.IIOReadWarningListener; -import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; @@ -61,7 +56,6 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; -import java.nio.charset.Charset; import java.util.*; import java.util.List; @@ -84,10 +78,17 @@ import java.util.List; * * Thumbnail support: * + * Metadata support: + * * * @author Harald Kuhr * @author LUT-based YCbCR conversion by Werner Randelshofer @@ -95,12 +96,10 @@ import java.util.List; * @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$ */ public class JPEGImageReader extends ImageReaderBase { - // TODO: Fix the (stream) metadata inconsistency issues. - // - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/ - // TODO: Allow automatic rotation based on EXIF rotation field? + // TODO: Create a simplified native metadata format that is closer to the actual JPEG stream AND supports EXIF in a sensible way - private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); + final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); /** Internal constant for referring all APP segments */ private static final int ALL_APP_MARKERS = -1; @@ -111,20 +110,6 @@ public class JPEGImageReader extends ImageReaderBase { private static Map> createSegmentIds() { Map> map = new LinkedHashMap>(); - /* - // JFIF/JFXX APP0 markers - map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS); - - // Exif metadata - map.put(JPEG.APP1, Collections.singletonList("Exif")); - - // ICC Color Profile - map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE")); - - // Adobe APP14 marker - map.put(JPEG.APP14, Collections.singletonList("Adobe")); - //*/ - // Need all APP markers to be able to re-generate proper metadata later for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) { map.put(appMarker, JPEGSegmentUtil.ALL_IDS); @@ -150,16 +135,19 @@ public class JPEGImageReader extends ImageReaderBase { /** Our JPEG reading delegate */ private final ImageReader delegate; - private ImageReader thumbnailReader; /** Listens to progress updates in the delegate, and delegates back to this instance */ private final ProgressDelegator progressDelegator; - /** Cached JPEG app segments */ - private List segments; - + /** Extra delegate for reading JPEG encoded thumbnails */ + private ImageReader thumbnailReader; private List thumbnails; + private JPEGImage10MetadataCleaner metadataCleaner; + + /** Cached list of JPEG segments we filter from the underlying stream */ + private List segments; + JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { super(provider); this.delegate = Validate.notNull(delegate); @@ -183,6 +171,8 @@ public class JPEGImageReader extends ImageReaderBase { thumbnailReader.reset(); } + metadataCleaner = null; + installListeners(); } @@ -712,7 +702,7 @@ public class JPEGImageReader extends ImageReaderBase { return appSegments; } - private SOFSegment getSOF() throws IOException { + SOFSegment getSOF() throws IOException { for (JPEGSegment segment : segments) { if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 || JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 || @@ -748,7 +738,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private AdobeDCTSegment getAdobeDCT() throws IOException { + AdobeDCTSegment getAdobeDCT() throws IOException { // TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers List adobe = getAppSegments(JPEG.APP14, "Adobe"); @@ -767,7 +757,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private JFIFSegment getJFIF() throws IOException{ + JFIFSegment getJFIF() throws IOException{ List jfif = getAppSegments(JPEG.APP0, "JFIF"); if (!jfif.isEmpty()) { @@ -778,7 +768,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private JFXXSegment getJFXX() throws IOException { + JFXXSegment getJFXX() throws IOException { List jfxx = getAppSegments(JPEG.APP0, "JFXX"); if (!jfxx.isEmpty()) { @@ -821,7 +811,7 @@ public class JPEGImageReader extends ImageReaderBase { return data; } - private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { + ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { // ICC v 1.42 (2006) annex B: // APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination) // + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments) @@ -994,7 +984,7 @@ public class JPEGImageReader extends ImageReaderBase { } } - private ImageReader getThumbnailReader() throws IOException { + ImageReader getThumbnailReader() throws IOException { if (thumbnailReader == null) { thumbnailReader = delegate.getOriginatingProvider().createReaderInstance(); } @@ -1039,243 +1029,19 @@ public class JPEGImageReader extends ImageReaderBase { @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { - // TODO: Extract metadata handling in separate class, for less mess and easier testing - // We filter out pretty much everything from the stream.. - // Meaning we have to read *all APP segments* and re-insert into metadata. + IIOMetadata imageMetadata = delegate.getImageMetadata(imageIndex); - // TODO: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... - // TODO: 1: Work around - // TODO: 2: REPORT BUG! - - List appSegments = getAppSegments(ALL_APP_MARKERS, null); -// System.out.println("appSegments: " + appSegments); - - IIOMetadata metadata = delegate.getImageMetadata(imageIndex); - - if (metadata != null) { - String format = metadata.getNativeMetadataFormatName(); - IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); - IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0); - IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0); - - JFIFSegment jfifSegment = getJFIF(); - JFXXSegment jfxxSegment = getJFXX(); - AdobeDCTSegment adobeDCT = getAdobeDCT(); - ICC_Profile embeddedICCProfile = getEmbeddedICCProfile(true); - SOFSegment sof = getSOF(); - - boolean hasRealJFIF = false; - boolean hasRealJFXX = false; - boolean hasRealICC = false; - - if (jfifSegment != null) { - // Normal case, conformant JFIF with 1 or 3 components - // TODO: Test if we have CMY or other bad color space? - // TODO: Remove JFIF if app14Adobe transform is YCCK (and isn't incorrect...) - if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) { - IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF"); - jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); - jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); - jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); - jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); - jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); - jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); - jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); - - jpegVariety.appendChild(jfif); - hasRealJFIF = true; - - // Add app2ICC and JFXX as proper nodes - if (embeddedICCProfile != null) { - IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); - app2ICC.setUserObject(embeddedICCProfile); - jfif.appendChild(app2ICC); - hasRealICC = true; - } - - if (jfxxSegment != null) { - IIOMetadataNode JFXX = new IIOMetadataNode("JFXX"); - jfif.appendChild(JFXX); - IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); - app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); - - JFXXThumbnailReader reader = new JFXXThumbnailReader(null, getThumbnailReader(), imageIndex, -1, jfxxSegment); - - IIOMetadataNode jfifThumb; - switch (jfxxSegment.extensionCode) { - case JFXXSegment.JPEG: - jfifThumb = new IIOMetadataNode("JFIFthumbJPEG"); - // Contains it's own "markerSequence" with full DHT, DQT, SOF etc... - IIOMetadata thumbMeta = reader.readMetadata(); - Node thumbTree = thumbMeta.getAsTree(format); - jfifThumb.appendChild(thumbTree.getLastChild()); - app0JFXX.appendChild(jfifThumb); - break; - - case JFXXSegment.INDEXED: - jfifThumb = new IIOMetadataNode("JFIFthumbPalette"); - jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); - jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); - app0JFXX.appendChild(jfifThumb); - break; - - case JFXXSegment.RGB: - jfifThumb = new IIOMetadataNode("JFIFthumbRGB"); - jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); - jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); - app0JFXX.appendChild(jfifThumb); - break; - - default: - processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode)); - } - - JFXX.appendChild(app0JFXX); - hasRealJFXX = true; - } - } - else { - // Typically CMYK with JFIF segment (Adobe or similar). - processWarningOccurred(String.format( - "Incompatible JFIF marker segment in stream. " + - "SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.", - sof.marker & 0xf, sof.componentsInFrame() - )); - } + if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) { + if (metadataCleaner == null) { + metadataCleaner = new JPEGImage10MetadataCleaner(this); } - // Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF - if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) { - processWarningOccurred(String.format( - "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + - "Ignoring Adobe App14 marker.", - sof.marker & 0xf, sof.componentsInFrame() - )); + List appSegments = getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null); - // Remove bad AdobeDCT - NodeList app14Adobe = tree.getElementsByTagName("app14Adobe"); - for (int i = app14Adobe.getLength() - 1; i >= 0; i--) { - Node item = app14Adobe.item(i); - item.getParentNode().removeChild(item); - } - - - // TODO: Add app14 as "unknown" marker? -// IIOMetadataNode app14Adobe = new IIOMetadataNode("app14Adobe"); -// app14Adobe.setAttribute("version", String.valueOf(adobeDCT.getVersion())); -// app14Adobe.setAttribute("flags0", String.valueOf(adobeDCT.getFlags0())); -// app14Adobe.setAttribute("flags1", String.valueOf(adobeDCT.getFlags1())); -// app14Adobe.setAttribute("transform", String.valueOf(sof.componentsInFrame() == 3 ? )); -// markerSequence.appendChild(app14Adobe); - } - - Node next = null; - for (JPEGSegment segment : appSegments) { - // TODO: Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers - if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) { - continue; - } - else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) { - continue; - } - else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) { - continue; - } - else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) { - continue; - } - else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) { - continue; - } - - IIOMetadataNode unknown = new IIOMetadataNode("unknown"); - unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff)); - - DataInputStream stream = new DataInputStream(segment.data()); - - try { - String identifier = segment.identifier(); - int off = identifier != null ? identifier.length() + 1 : 0; - - byte[] data = new byte[off + segment.length()]; - - if (identifier != null) { - System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length()); - } - - stream.readFully(data, off, segment.length()); - - unknown.setUserObject(data); - } - finally { - stream.close(); - } - - if (next == null) { - next = markerSequence.getFirstChild(); - } - - markerSequence.insertBefore(unknown, next); - } - - // Inconsistency issue in the com.sun classes, it can read metadata with dht containing - // more than 4 children, but will not allow setting such a tree... - // We'll split AC/DC tables into separate dht nodes. - NodeList dhts = markerSequence.getElementsByTagName("dht"); - for (int j = 0; j < dhts.getLength(); j++) { - Node dht = dhts.item(j); - NodeList dhtables = dht.getChildNodes(); - - if (dhtables.getLength() > 4) { - IIOMetadataNode acTables = new IIOMetadataNode("dht"); - dht.getParentNode().insertBefore(acTables, dht.getNextSibling()); - - - // Split into 2 dht nodes, one for AC and one for DC - for (int i = 0; i < dhtables.getLength(); i++) { - Element dhtable = (Element) dhtables.item(i); - String tableClass = dhtable.getAttribute("class"); - if ("1".equals(tableClass)) { - dht.removeChild(dhtable); - acTables.appendChild(dhtable); - } - } - } - } - - // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. - // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: - // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata - /* - from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html - - In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) - by defining other types of nodes which may appear as a child of the JPEGvariety node. - - (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the - javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an - APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific - code to interpret the data in the marker segment. If such an application were to encounter a metadata tree - formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be - unknown in that format - it might be structured as a child node of the JPEGvariety node. - - Thus, it is important for an application to specify which version to use by passing the string identifying - the version to the method/constructor used to obtain an IIOMetadata object.) - */ - -// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); - - try { - metadata.setFromTree(format, tree); - } - catch (IIOInvalidTreeException e) { - new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); - throw e; - } -// metadata.mergeTree(format, tree); // TODO: Merging does not work, as the "unknown" tags duplicate insert bug ruins everything. Try set instead... + return metadataCleaner.cleanMetadata(imageIndex, imageMetadata, appSegments); } - return metadata; + return imageMetadata; } @Override @@ -1283,6 +1049,11 @@ public class JPEGImageReader extends ImageReaderBase { return delegate.getStreamMetadata(); } + @Override + protected void processWarningOccurred(String warning) { + super.processWarningOccurred(warning); + } + private static void invertCMYK(final Raster raster) { byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); @@ -1612,7 +1383,8 @@ public class JPEGImageReader extends ImageReaderBase { showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); } - reader.getImageMetadata(0); + IIOMetadata imageMetadata = reader.getImageMetadata(0); + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()), false); } catch (IIOException e) { System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage());