-
Bug
-
Resolution: Unresolved
-
P4
-
8, 11, 17, 21
-
generic
-
generic
A DESCRIPTION OF THE PROBLEM :
Duplicate JFIF marker after call JPEGMetadata.setFromTree
I need to write JPEG stream with RGB color space (RGB-encoded JPEG tiles are popular inside TIFF). I've tried the following way to create such JPEG, that seems better that a "trick" way that I offered in https://bugs.java.com/bugdatabase/view_bug?bug_id=JDK-8313303
Now I read JPEG metadata with help of call
Node tree = metadata.getAsTree("javax_imageio_1.0")
then I correct the node Chroma/ColorSpaceType to "RGB", as described here:
https://docs.oracle.com/javase%2F9%2Fdocs%2Fapi%2F%2F/javax/imageio/metadata/doc-files/jpeg_metadata.html#tree
Then I write metadata back with help of
metadata.setFromTree("javax_imageio_1.0", tree)
See the deatailed source code below.
Unfortunately, this techique leads to a bug, when the color space type is actually not changed! Default color space in the metadata, returned by writer.getDefaultImageMetadata, is "YCbCr" for JPEG format. When we pass "YCbCr" as an argument of my correctColorSpace method, metadata.setFromTree call writes JFIF marker into native JPEG metadata TWICE. And The same problem occurs in the very simple correctColorSpaceDummy method, which do not try to correct anything and just calls getAsTree and then setFromTree with the same metadata.
This is the main bug #1. JPEG should not contain TWO markers JFIF.
However, this JPEG is written successfully by the following call
writer.write(null, iioImage, writeParam)
I think this is the bug #2. Really, when I try to read such "strange" (more exactly, invalid) JPEG file back by AWT ImageReader and then read its metadata by reader.getImageMetadata call, I see the following exception:
Exception in thread "main" javax.imageio.IIOException: JFIF APP0 must be first marker after SOI
at java.desktop/com.sun.imageio.plugins.jpeg.JPEGMetadata.<init>(JPEGMetadata.java:223)
at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.getImageMetadata(JPEGImageReader.java:1145)
at net.algart.matrices.io.formats.tests.bugs.AWTJpegMetadataBug.main(AWTJpegMetadataBug.java:156)
I beleive that AWT JPEGImageWriter must not silently write JPEG file, which cannot be normally analysed by further usage of JPEGImageReader. It is much better if JPEGImageWriter would detect duplicated JFIF marker and throw an exception.
It seems that the reason of the bug #1 is in the following code inside your JPEGMetadata class, mergeStandardChromaNode method:
// 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());
}
Here is no any check that JFIF is ALREADY present in the sequence, and it is just added 2nd time.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Please call my test AWTReadMetadataTest with two arguments: 1st is a file with any picture (not important), 2nd is the name of test JPEG file, where this picture will be written.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Normal work both with NEED_JCS_RGB = false and NEED_JCS_RGB = true
ACTUAL -
Exception while attempt to analyze metadata of the new JPEG file, written when NEED_JCS_RGB = false;
---------- BEGIN SOURCE ----------
public class AWTJpegMetadataBug {
public static final boolean NEED_JCS_RGB = false;
// Please set to "false" to enforce bug.
// Please set fo "true" to write RGB-encoded JPEG file.
// This method ALWAYS leads to a bug: just try to call it instead of correctColorSpace below
static void correctColorSpaceDummy(IIOMetadata metadata, String colorSpace) throws IIOInvalidTreeException {
Node tree = metadata.getAsTree("javax_imageio_1.0");
metadata.setFromTree("javax_imageio_1.0", tree); // - leads to duplicate APP0!
}
// This method leads to a bug if colorSpace is actually the same
static void correctColorSpace(IIOMetadata metadata, String colorSpace) throws IIOInvalidTreeException {
Node tree = metadata.getAsTree("javax_imageio_1.0");
NodeList rootNodes = tree.getChildNodes();
for (int k = 0, n = rootNodes.getLength(); k < n; k++) {
Node rootChild = rootNodes.item(k);
String childName = rootChild.getNodeName();
if ("Chroma".equalsIgnoreCase(childName)) {
NodeList nodes = rootChild.getChildNodes();
for (int i = 0, m = nodes.getLength(); i < m; i++) {
Node subChild = nodes.item(i);
String subChildName = subChild.getNodeName();
if ("ColorSpaceType".equalsIgnoreCase(subChildName)) {
NamedNodeMap attributes = subChild.getAttributes();
Node name = attributes.getNamedItem("name");
name.setNodeValue(colorSpace);
}
}
}
}
metadata.setFromTree("javax_imageio_1.0", tree);
// !!!! BUG #1: if the tree was not actually changed and still contains "YCbCr" as ColorSpaceType,
// !!!! setFromTree writes JFIF marker into the native metadata twice!
}
// Writes image either in usual YCbCr or in RGB color space
public static void writeJpegViaMetadata(BufferedImage image, File file, boolean rgbSpace) throws IOException {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
if (!writers.hasNext()) {
throw new IIOException("Cannot write JPEG");
}
ImageWriter writer = writers.next();
ImageOutputStream ios = ImageIO.createImageOutputStream(file);
writer.setOutput(ios);
ImageTypeSpecifier imageTypeSpecifier = new ImageTypeSpecifier(image);
ImageWriteParam writeParam = writer.getDefaultWriteParam();
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionType("JPEG");
IIOMetadata metadata = writer.getDefaultImageMetadata(imageTypeSpecifier, writeParam);
correctColorSpace(metadata, rgbSpace ? "RGB" : "YCbCr");
// - lead to invalid metadata (duplicate APP0) in a case YCbCr
IIOImage iioImage = new IIOImage(image, null, metadata);
writer.write(null, iioImage, writeParam);
// !!!! BUG #2: even when metadata are incorrect and contain duplicated JFIF marker,
// !!!! this "write" method works without exceptions and creates "strange" JPEG
}
private static String metadataToString(IIOMetadata metadata, String formatName) {
if (metadata == null) {
return "no metadata";
}
Node node = metadata.getAsTree(formatName);
StringWriter sw = new StringWriter();
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.transform(new DOMSource(node), new StreamResult(sw));
return sw.toString();
} catch (TransformerException e) {
throw new IllegalArgumentException(e);
}
}
public static void main(String[] args) throws IOException {
if (args.length < 2) {
System.out.println("Usage:");
System.out.println(" " + AWTJpegMetadataBug.class.getName()
+ " some_image.jpeg result.jpeg");
return;
}
final File srcFile = new File(args[0]);
final File resultFile = new File(args[1]);
BufferedImage bi = ImageIO.read(srcFile);
if (bi == null) {
throw new IOException("Cannot read " + srcFile);
}
resultFile.delete();
writeJpegViaMetadata(bi, resultFile, NEED_JCS_RGB);
// Try to read metadata from the written file:
ImageInputStream stream = ImageIO.createImageInputStream(resultFile);
if (stream == null) {
throw new IIOException("Can't create an ImageInputStream!");
}
Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
throw new IIOException("Can't read " + resultFile);
}
ImageReader reader = iter.next();
reader.setInput(stream, true, true);
IIOMetadata imageMetadata = reader.getImageMetadata(0);
// !!!! BUG #1 becomes visible here (for YCbCr color space):
// !!!! "javax.imageio.IIOException: JFIF APP0 must be first marker after SOI"
System.out.printf("Successfully loaded back! Metadata:%n%s%n",
metadataToString(imageMetadata, "javax_imageio_jpeg_image_1.0"));
}
}
---------- END SOURCE ----------
FREQUENCY : always
Duplicate JFIF marker after call JPEGMetadata.setFromTree
I need to write JPEG stream with RGB color space (RGB-encoded JPEG tiles are popular inside TIFF). I've tried the following way to create such JPEG, that seems better that a "trick" way that I offered in https://bugs.java.com/bugdatabase/view_bug?bug_id=JDK-8313303
Now I read JPEG metadata with help of call
Node tree = metadata.getAsTree("javax_imageio_1.0")
then I correct the node Chroma/ColorSpaceType to "RGB", as described here:
https://docs.oracle.com/javase%2F9%2Fdocs%2Fapi%2F%2F/javax/imageio/metadata/doc-files/jpeg_metadata.html#tree
Then I write metadata back with help of
metadata.setFromTree("javax_imageio_1.0", tree)
See the deatailed source code below.
Unfortunately, this techique leads to a bug, when the color space type is actually not changed! Default color space in the metadata, returned by writer.getDefaultImageMetadata, is "YCbCr" for JPEG format. When we pass "YCbCr" as an argument of my correctColorSpace method, metadata.setFromTree call writes JFIF marker into native JPEG metadata TWICE. And The same problem occurs in the very simple correctColorSpaceDummy method, which do not try to correct anything and just calls getAsTree and then setFromTree with the same metadata.
This is the main bug #1. JPEG should not contain TWO markers JFIF.
However, this JPEG is written successfully by the following call
writer.write(null, iioImage, writeParam)
I think this is the bug #2. Really, when I try to read such "strange" (more exactly, invalid) JPEG file back by AWT ImageReader and then read its metadata by reader.getImageMetadata call, I see the following exception:
Exception in thread "main" javax.imageio.IIOException: JFIF APP0 must be first marker after SOI
at java.desktop/com.sun.imageio.plugins.jpeg.JPEGMetadata.<init>(JPEGMetadata.java:223)
at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.getImageMetadata(JPEGImageReader.java:1145)
at net.algart.matrices.io.formats.tests.bugs.AWTJpegMetadataBug.main(AWTJpegMetadataBug.java:156)
I beleive that AWT JPEGImageWriter must not silently write JPEG file, which cannot be normally analysed by further usage of JPEGImageReader. It is much better if JPEGImageWriter would detect duplicated JFIF marker and throw an exception.
It seems that the reason of the bug #1 is in the following code inside your JPEGMetadata class, mergeStandardChromaNode method:
// 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());
}
Here is no any check that JFIF is ALREADY present in the sequence, and it is just added 2nd time.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Please call my test AWTReadMetadataTest with two arguments: 1st is a file with any picture (not important), 2nd is the name of test JPEG file, where this picture will be written.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Normal work both with NEED_JCS_RGB = false and NEED_JCS_RGB = true
ACTUAL -
Exception while attempt to analyze metadata of the new JPEG file, written when NEED_JCS_RGB = false;
---------- BEGIN SOURCE ----------
public class AWTJpegMetadataBug {
public static final boolean NEED_JCS_RGB = false;
// Please set to "false" to enforce bug.
// Please set fo "true" to write RGB-encoded JPEG file.
// This method ALWAYS leads to a bug: just try to call it instead of correctColorSpace below
static void correctColorSpaceDummy(IIOMetadata metadata, String colorSpace) throws IIOInvalidTreeException {
Node tree = metadata.getAsTree("javax_imageio_1.0");
metadata.setFromTree("javax_imageio_1.0", tree); // - leads to duplicate APP0!
}
// This method leads to a bug if colorSpace is actually the same
static void correctColorSpace(IIOMetadata metadata, String colorSpace) throws IIOInvalidTreeException {
Node tree = metadata.getAsTree("javax_imageio_1.0");
NodeList rootNodes = tree.getChildNodes();
for (int k = 0, n = rootNodes.getLength(); k < n; k++) {
Node rootChild = rootNodes.item(k);
String childName = rootChild.getNodeName();
if ("Chroma".equalsIgnoreCase(childName)) {
NodeList nodes = rootChild.getChildNodes();
for (int i = 0, m = nodes.getLength(); i < m; i++) {
Node subChild = nodes.item(i);
String subChildName = subChild.getNodeName();
if ("ColorSpaceType".equalsIgnoreCase(subChildName)) {
NamedNodeMap attributes = subChild.getAttributes();
Node name = attributes.getNamedItem("name");
name.setNodeValue(colorSpace);
}
}
}
}
metadata.setFromTree("javax_imageio_1.0", tree);
// !!!! BUG #1: if the tree was not actually changed and still contains "YCbCr" as ColorSpaceType,
// !!!! setFromTree writes JFIF marker into the native metadata twice!
}
// Writes image either in usual YCbCr or in RGB color space
public static void writeJpegViaMetadata(BufferedImage image, File file, boolean rgbSpace) throws IOException {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
if (!writers.hasNext()) {
throw new IIOException("Cannot write JPEG");
}
ImageWriter writer = writers.next();
ImageOutputStream ios = ImageIO.createImageOutputStream(file);
writer.setOutput(ios);
ImageTypeSpecifier imageTypeSpecifier = new ImageTypeSpecifier(image);
ImageWriteParam writeParam = writer.getDefaultWriteParam();
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionType("JPEG");
IIOMetadata metadata = writer.getDefaultImageMetadata(imageTypeSpecifier, writeParam);
correctColorSpace(metadata, rgbSpace ? "RGB" : "YCbCr");
// - lead to invalid metadata (duplicate APP0) in a case YCbCr
IIOImage iioImage = new IIOImage(image, null, metadata);
writer.write(null, iioImage, writeParam);
// !!!! BUG #2: even when metadata are incorrect and contain duplicated JFIF marker,
// !!!! this "write" method works without exceptions and creates "strange" JPEG
}
private static String metadataToString(IIOMetadata metadata, String formatName) {
if (metadata == null) {
return "no metadata";
}
Node node = metadata.getAsTree(formatName);
StringWriter sw = new StringWriter();
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.transform(new DOMSource(node), new StreamResult(sw));
return sw.toString();
} catch (TransformerException e) {
throw new IllegalArgumentException(e);
}
}
public static void main(String[] args) throws IOException {
if (args.length < 2) {
System.out.println("Usage:");
System.out.println(" " + AWTJpegMetadataBug.class.getName()
+ " some_image.jpeg result.jpeg");
return;
}
final File srcFile = new File(args[0]);
final File resultFile = new File(args[1]);
BufferedImage bi = ImageIO.read(srcFile);
if (bi == null) {
throw new IOException("Cannot read " + srcFile);
}
resultFile.delete();
writeJpegViaMetadata(bi, resultFile, NEED_JCS_RGB);
// Try to read metadata from the written file:
ImageInputStream stream = ImageIO.createImageInputStream(resultFile);
if (stream == null) {
throw new IIOException("Can't create an ImageInputStream!");
}
Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
throw new IIOException("Can't read " + resultFile);
}
ImageReader reader = iter.next();
reader.setInput(stream, true, true);
IIOMetadata imageMetadata = reader.getImageMetadata(0);
// !!!! BUG #1 becomes visible here (for YCbCr color space):
// !!!! "javax.imageio.IIOException: JFIF APP0 must be first marker after SOI"
System.out.printf("Successfully loaded back! Metadata:%n%s%n",
metadataToString(imageMetadata, "javax_imageio_jpeg_image_1.0"));
}
}
---------- END SOURCE ----------
FREQUENCY : always
- relates to
-
JDK-8313303 Invalid behaviour of JPEGImageWriter.getDefaultImageMetadata
- Open