diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOImageWriter.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOImageWriter.java index d6cd92ad7..d7abbbe90 100644 --- a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOImageWriter.java +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOImageWriter.java @@ -59,19 +59,11 @@ public ImageIOImageWriter(String mime) { this.targetMIME = mime; } - /** - * @see ImageWriter#writeImage(java.awt.image.RenderedImage, - * java.io.OutputStream) - */ @Override public void writeImage(RenderedImage image, OutputStream out) throws IOException { writeImage(image, out, null); } - /** - * @see ImageWriter#writeImage(java.awt.image.RenderedImage, - * java.io.OutputStream, ImageWriterParams) - */ @Override public void writeImage(RenderedImage image, OutputStream out, ImageWriterParams params) throws IOException { Iterator iter = ImageIO.getImageWritersByMIMEType(getMIMEType()); diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOJPEGImageWriter.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOJPEGImageWriter.java index 3246a5ca6..ee3959c3b 100644 --- a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOJPEGImageWriter.java +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOJPEGImageWriter.java @@ -127,13 +127,16 @@ private static IIOMetadata addAdobeTransform(IIOMetadata meta) { protected ImageWriteParam getDefaultWriteParam(ImageWriter iiowriter, RenderedImage image, ImageWriterParams params) { JPEGImageWriteParam param = new JPEGImageWriteParam(iiowriter.getLocale()); - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionQuality(params.getJPEGQuality()); - if (params.getCompressionMethod() != null && !"JPEG".equals(params.getCompressionMethod())) { - throw new IllegalArgumentException("No compression method other than JPEG is supported for JPEG output!"); - } - if (params.getJPEGForceBaseline()) { - param.setProgressiveMode(ImageWriteParam.MODE_DISABLED); + if (params != null) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(params.getJPEGQuality()); + if (params.getCompressionMethod() != null && !"JPEG".equals(params.getCompressionMethod())) { + throw new IllegalArgumentException( + "No compression method other than JPEG is supported for JPEG output!"); + } + if (params.getJPEGForceBaseline()) { + param.setProgressiveMode(ImageWriteParam.MODE_DISABLED); + } } return param; } diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOPNGImageWriter.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOPNGImageWriter.java index 47f9eb113..65f6f8317 100644 --- a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOPNGImageWriter.java +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/ImageIOPNGImageWriter.java @@ -30,6 +30,8 @@ import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; +import io.sf.carte.echosvg.ext.awt.image.codec.impl.ColorUtil; + /** * ImageWriter that encodes PNG images using Image I/O. * @@ -47,7 +49,7 @@ public ImageIOPNGImageWriter() { @Override protected void updateColorMetadata(IIOMetadata meta, ColorSpace colorSpace) { - if (!colorSpace.isCS_sRGB() && colorSpace instanceof ICC_ColorSpace + if (!ColorUtil.isBuiltInColorSpace(colorSpace) && colorSpace instanceof ICC_ColorSpace && meta.isStandardMetadataFormatSupported()) { final String metaName = "javax_imageio_png_1.0"; IIOMetadataNode root = (IIOMetadataNode) meta.getAsTree(metaName); diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/ColorUtil.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/ColorUtil.java new file mode 100644 index 000000000..06a27b273 --- /dev/null +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/ColorUtil.java @@ -0,0 +1,49 @@ +/* + + See the NOTICE file distributed with this work for additional + information regarding copyright ownership. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package io.sf.carte.echosvg.ext.awt.image.codec.impl; + +import java.awt.color.ColorSpace; + +/** + * Color utilities. + * + * @version $Id$ + */ +public class ColorUtil { + + /** + * Avoid instantiation. + */ + ColorUtil() { + super(); + } + + /** + * + * @param colorSpace the color space. + * @return {@code true} if it is a built-in color space. + */ + public static boolean isBuiltInColorSpace(ColorSpace colorSpace) { + return colorSpace.isCS_sRGB() || colorSpace == ColorSpace.getInstance(ColorSpace.CS_CIEXYZ) + || colorSpace == ColorSpace.getInstance(ColorSpace.CS_GRAY) + || colorSpace == ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB) + || colorSpace == ColorSpace.getInstance(ColorSpace.CS_PYCC); + } + +} diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/package-info.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/package-info.java new file mode 100644 index 000000000..5aafa827d --- /dev/null +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/impl/package-info.java @@ -0,0 +1,22 @@ +/* + + See the NOTICE file distributed with this work for additional + information regarding copyright ownership. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +/** + * Implementation classes, do not use outside of the codec module. + */ +package io.sf.carte.echosvg.ext.awt.image.codec.impl; diff --git a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/png/PNGImageEncoder.java b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/png/PNGImageEncoder.java index 41f9d8211..aea6bdeaa 100644 --- a/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/png/PNGImageEncoder.java +++ b/echosvg-codec/src/main/java/io/sf/carte/echosvg/ext/awt/image/codec/png/PNGImageEncoder.java @@ -42,6 +42,7 @@ import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; +import io.sf.carte.echosvg.ext.awt.image.codec.impl.ColorUtil; import io.sf.carte.echosvg.ext.awt.image.codec.util.ImageEncoderImpl; import io.sf.carte.echosvg.ext.awt.image.codec.util.PropertyUtil; @@ -1093,7 +1094,7 @@ public void encode(RenderedImage im) throws IOException { private void setICCProfileInfo(ColorModel colorModel) { ColorSpace cs = colorModel.getColorSpace(); - if (!cs.isCS_sRGB() && cs instanceof ICC_ColorSpace) { + if (!ColorUtil.isBuiltInColorSpace(cs) && cs instanceof ICC_ColorSpace) { ICC_Profile profile = ((ICC_ColorSpace) cs).getProfile(); byte[] bdesc = profile.getData(ICC_Profile.icSigProfileDescriptionTag); /* @@ -1113,13 +1114,13 @@ private void setICCProfileInfo(ColorModel colorModel) { String desc = new String(bdesc, offset, len, StandardCharsets.UTF_16BE).trim(); iccProfileName = desc; iccProfileData = profile.getData(); + return; } } } - } else { - iccProfileName = null; - iccProfileData = null; } + iccProfileName = null; + iccProfileData = null; } /** diff --git a/echosvg-test/src/main/java/io/sf/carte/echosvg/test/TestUtil.java b/echosvg-test/src/main/java/io/sf/carte/echosvg/test/TestUtil.java index a1f6add7b..606f9aa2a 100644 --- a/echosvg-test/src/main/java/io/sf/carte/echosvg/test/TestUtil.java +++ b/echosvg-test/src/main/java/io/sf/carte/echosvg/test/TestUtil.java @@ -90,7 +90,7 @@ public static String getProjectBuildURL(Class cl, String projectDirname) { String classUrl; if (url == null) { url = cwdURL(); - File f = new File(url.getFile(), projectDirname); + File f = new File(url.getFile(), projectDirname); if (f.exists()) { // CWD is root directory try { diff --git a/echosvg-test/src/main/java/io/sf/carte/echosvg/test/image/ImageComparator.java b/echosvg-test/src/main/java/io/sf/carte/echosvg/test/image/ImageComparator.java index a8fcad53e..5e725d70b 100644 --- a/echosvg-test/src/main/java/io/sf/carte/echosvg/test/image/ImageComparator.java +++ b/echosvg-test/src/main/java/io/sf/carte/echosvg/test/image/ImageComparator.java @@ -34,6 +34,10 @@ * comparisons with an arbitrary number of variants to match. See * {@link ImageVariants} for a description of the variants mechanism. *

+ *

+ * This software was written for image comparisons in the context of the EchoSVG + * project. Usage outside of that context is not supported. + *

*/ public class ImageComparator { @@ -619,7 +623,7 @@ public static String getResultDescription(short code) { * @return an image comparing the two given images, one on each side. */ public static BufferedImage createCompareImage(BufferedImage ref, BufferedImage gen) { - BufferedImage cmp = new BufferedImage(ref.getWidth() * 2, ref.getHeight(), BufferedImage.TYPE_INT_ARGB); + BufferedImage cmp = createImageOfType(ref.getWidth() * 2, ref.getHeight(), ref); Graphics2D g = cmp.createGraphics(); g.setPaint(Color.white); @@ -650,7 +654,8 @@ public static BufferedImage createDiffImage(BufferedImage ref, BufferedImage gen final int w = ref.getWidth(); final int h = ref.getHeight(); - BufferedImage diff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + BufferedImage diff = createImageOfType(w, h, ref); + WritableRaster refWR = ref.getRaster(); WritableRaster genWR = gen.getRaster(); WritableRaster dstWR = diff.getRaster(); @@ -701,6 +706,21 @@ public static BufferedImage createDiffImage(BufferedImage ref, BufferedImage gen return diff; } + private static BufferedImage createImageOfType(int width, int height, BufferedImage ref) { + BufferedImage image; + + int type = ref.getType(); + if (type != BufferedImage.TYPE_CUSTOM) { + image = new BufferedImage(width, height, type); + } else { + ColorModel cm = ref.getColorModel(); + WritableRaster raster = cm.createCompatibleWritableRaster(width, height); + image = new BufferedImage(cm, raster, false, null); + } + + return image; + } + /** * Creates a new image that is the exact difference between the two input * images. @@ -722,7 +742,8 @@ public static BufferedImage createExactDiffImage(BufferedImage ref, BufferedImag "Ref. image has height " + h + " but generated image is " + gen.getHeight()); } - BufferedImage diff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + BufferedImage diff = createImageOfType(w, h, ref); + WritableRaster refWR = ref.getRaster(); WritableRaster genWR = gen.getRaster(); WritableRaster dstWR = diff.getRaster(); @@ -782,7 +803,8 @@ public static BufferedImage createMergedDiffImage(BufferedImage ref, BufferedIma final int w = ref.getWidth(); final int h = ref.getHeight(); - BufferedImage diff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + BufferedImage diff = createImageOfType(w, h, ref); + WritableRaster refWR = ref.getRaster(); WritableRaster genWR = gen.getRaster(); WritableRaster rangeWR = rangeDiff.getRaster(); diff --git a/echosvg-test/src/test/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/ImageIOJPEGImageWriterTest.java b/echosvg-test/src/test/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/ImageIOJPEGImageWriterTest.java new file mode 100644 index 000000000..840f58c5b --- /dev/null +++ b/echosvg-test/src/test/java/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/ImageIOJPEGImageWriterTest.java @@ -0,0 +1,279 @@ +/* + + See the NOTICE file distributed with this work for additional + information regarding copyright ownership. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package io.sf.carte.echosvg.ext.awt.image.codec.imageio.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Transparency; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentColorModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Test; + +import io.sf.carte.echosvg.ext.awt.image.codec.imageio.ImageIOJPEGImageWriter; +import io.sf.carte.echosvg.ext.awt.image.spi.ImageWriter; +import io.sf.carte.echosvg.ext.awt.image.spi.ImageWriterParams; +import io.sf.carte.echosvg.ext.awt.image.spi.ImageWriterRegistry; +import io.sf.carte.echosvg.test.TestLocations; +import io.sf.carte.echosvg.test.TestUtil; +import io.sf.carte.echosvg.test.image.ImageComparator; +import io.sf.carte.echosvg.test.image.ImageFileBuilder; +import io.sf.carte.echosvg.test.image.TempImageFiles; + +/** + * This test validates the ImageIOJPEGImageWriter operation. + * + *

The drawing is based on PNGEncoderTest by Vincent Hardy.

+ * + * @version $Id$ + */ +public class ImageIOJPEGImageWriterTest { + + private static final int width = 200; + + private static final int height = 150; + + @Test + public void test3B_BGR() throws IOException { + BufferedImage image = drawImage(new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR)); + testEncoding(image, "3bBgr"); + } + + @Test + public void testICC() throws IOException { + ICC_Profile prof; + try (InputStream iccStream = ImageIOJPEGImageWriterTest.class.getResourceAsStream( + "/io/sf/carte/echosvg/css/color/profiles/Display P3.icc")) { + prof = ICC_Profile.getInstance(iccStream); + } + ICC_ColorSpace cs = new ICC_ColorSpace(prof); + + int[] bits = { 8, 8, 8 }; + int[] offsets = { 2, 1, 0 }; + + ComponentColorModel cm = new ComponentColorModel(cs, bits, false, false, + Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + ComponentSampleModel sm = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, + 3, width * 3, offsets); + Point loc = new Point(0, 0); + WritableRaster raster = Raster.createWritableRaster(sm, loc); + + BufferedImage raw = new BufferedImage(cm, raster, false, null); + BufferedImage image = drawImage(raw); + + testEncoding(image, "icc"); + } + + BufferedImage drawImage(BufferedImage image) { + // Create a BufferedImage to be encoded + Graphics2D ig = image.createGraphics(); + ig.setPaint(new Color(128, 0, 0)); + ig.fillRect(0, 0, 100, 50); + ig.setPaint(Color.orange); + ig.fillRect(100, 0, 100, 50); + ig.setPaint(Color.yellow); + ig.fillRect(0, 50, 100, 50); + ig.setPaint(Color.red); + ig.fillRect(100, 50, 100, 50); + ig.setPaint(new Color(255, 127, 127)); + ig.fillRect(0, 100, 100, 50); + ig.setPaint(Color.black); + ig.draw(new Rectangle2D.Double(0.5, 0.5, 199, 149)); + ig.dispose(); + + return image; + } + + void testEncoding(final BufferedImage image, String baseName) throws IOException { + // Create an output stream where the JPEG data + // will be stored. + ByteArrayOutputStream bos = new ByteArrayOutputStream(256); + // Now, try to encode image + ImageWriter writer = new ImageIOJPEGImageWriter(); + ImageWriterParams params = new ImageWriterParams(); + params.setJPEGQuality(1f, false); + + writer.writeImage(image, bos, params); + + // Now compare with reference + assertTrue(checkIdentical(bos.toByteArray(), image, baseName), + "Encoded file does not match reference."); + } + + InputStream openRefStream(String baseName) { + String name = resourcePath(baseName); + return getClass().getResourceAsStream(name); + } + + String resourcePath(String baseName) { + return '/' + getClass().getPackageName().replace('.', '/') + '/' + baseName + getDotExtension(); + } + + protected String getDotExtension() { + return ".jpg"; + } + + protected String getMIMEType() { + return "image/jpeg"; + } + + /** + * Template method for building the JPEG input stream. + */ + protected InputStream buildInputStream(ByteArrayOutputStream bos) { + return new ByteArrayInputStream(bos.toByteArray()); + } + + /** + * Compares the streams of the two images + * + * @param cand byte array with the candidate image. + * @param baseName the reference file basename. + * @throws IOException if an I/O error happens. + */ + private boolean checkIdentical(byte[] cand, BufferedImage imgCand, String baseName) + throws IOException { + if (equalStreams(cand, baseName)) { + return true; + } + + // We are in error (images are different: produce an image + // with the two images side by side as well as a diff image). + + // First, create a reference image (which may be inaccurate due to + // ImageIO transcoding issues, but enough to give an idea). + BufferedImage imgRef; + try (InputStream isRef = openRefStream(baseName)) { + imgRef = decodeStream(isRef); + } + + BufferedImage diff = ImageComparator.createDiffImage(imgRef, imgCand); + BufferedImage cmp = ImageComparator.createCompareImage(imgRef, imgCand); + + ImageFileBuilder tmpUtil = new TempImageFiles( + TestUtil.getProjectBuildURL(getClass(), TestLocations.TEST_DIRNAME)); + + bytesToFile(cand, tmpUtil, baseName, "_candidate"); + imageToFile(diff, tmpUtil, baseName, "_diff"); + imageToFile(cmp, tmpUtil, baseName, "_cmp"); + + return false; + } + + protected BufferedImage decodeStream(InputStream is) throws IOException { + return ImageIO.read(is); + } + + private boolean equalStreams(byte[] cand, String baseName) throws IOException { + byte[] bufRef = new byte[4096]; + byte[] bufCand = new byte[4096]; + + InputStream isCand = new ByteArrayInputStream(cand); + try (InputStream isRef = openRefStream(baseName)) { + if (isRef == null) { + // No reference + ImageFileBuilder tmpUtil = new TempImageFiles( + TestUtil.getProjectBuildURL(getClass(), TestLocations.TEST_DIRNAME)); + bytesToFile(cand, tmpUtil, baseName, "_candidate"); + throw new FileNotFoundException("Cannot find reference resource at " + resourcePath(baseName)); + } + + int count; + while ((count = isRef.read(bufRef)) != -1) { + int candCount = isCand.read(bufCand, 0, count); + // We are reading the candidate from memory, count must be equal + if (candCount != count || !Arrays.equals(bufRef, 0, count, bufCand, 0, count)) { + return false; + } + } + + // Check that the candidate stream is empty + if (isCand.read() != -1) { + return false; + } + } + + return true; + } + + /** + * Creates a temporary File into which the bytes are saved. + */ + private File bytesToFile(byte[] cand, ImageFileBuilder fileBuilder, String name, String imageSuffix) + throws IOException { + + File imageFile = obtainDiffCmpFilename(fileBuilder, name, imageSuffix); + + try (OutputStream out = new FileOutputStream(imageFile)) { + out.write(cand); + } + + return imageFile; + + } + + /** + * Creates a temporary File into which the input image is saved. + */ + private File imageToFile(BufferedImage img, ImageFileBuilder fileBuilder, String name, String imageSuffix) + throws IOException { + + File imageFile = obtainDiffCmpFilename(fileBuilder, name, imageSuffix); + + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(getMIMEType()); + + try (OutputStream out = new FileOutputStream(imageFile)) { + writer.writeImage(img, out); + } + + return imageFile; + + } + + /** + * Creates a temporary File into which the input image is saved. + */ + private File obtainDiffCmpFilename(ImageFileBuilder fileBuilder, String name, String imageSuffix) + throws IOException { + return fileBuilder.createImageFile(name + imageSuffix + getDotExtension()); + } + +} diff --git a/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/3bBgr.jpg b/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/3bBgr.jpg new file mode 100644 index 000000000..4a3fc28ea Binary files /dev/null and b/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/3bBgr.jpg differ diff --git a/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/icc.jpg b/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/icc.jpg new file mode 100644 index 000000000..7e0cc6dea Binary files /dev/null and b/test-resources/io/sf/carte/echosvg/ext/awt/image/codec/imageio/test/icc.jpg differ