From 2d04b8d4846ed4aa56f01cee326d4f59e6a83129 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 23 Feb 2011 19:16:47 +0100 Subject: [PATCH] Added fast conversion from CMYK to RGB for non-ICC cases. --- .../imageio/plugins/jpeg/FastCMYKToRGB.java | 189 ++++++++++++++ .../plugins/jpeg/FastCMYKToRGBTest.java | 235 ++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java create mode 100644 imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java new file mode 100644 index 00000000..5897a7aa --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.lang.Validate; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; + +/** + * This class performs a pixel by pixel conversion of the source image, from CMYK to RGB. + *

+ * The conversion is fast, but performed without any color space conversion. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FastCMYKToRGB.java,v 1.0 21.02.11 13.22 haraldk Exp$ + * @see ColorConvertOp + */ +class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { + // TODO: Force dest alpha to match source alpha? + + public FastCMYKToRGB() { + } + + /** + * Converts the CMYK source raster to the destination RGB raster. + * + * @param src assumed to be 4 byte CMYK + * @param dest raster, in either 3 byte BGR/BGR, 4 byte ABGR or int RGB/ARGB format, or {@code null} + * @return {@code dest}, or a new {@link WritableRaster} if {@code dest} is {@code null}. + * @throws IllegalArgumentException if {@code src} and {@code dest} refer to the same object + */ + public WritableRaster filter(Raster src, WritableRaster dest) { + Validate.notNull(src, "src may not be null"); + // TODO: Why not allow same raster, if converting to 4 byte ABGR? + Validate.isTrue(src != dest, "src and dest raster may not be same"); + Validate.isTrue(src.getTransferType() == DataBuffer.TYPE_BYTE, src, "only TYPE_BYTE rasters supported as src: %s"); + Validate.isTrue(src.getNumDataElements() >= 4, src.getNumDataElements(), "CMYK raster must have at least 4 data elements: %s"); + + if (dest == null) { + dest = createCompatibleDestRaster(src); + } + else { + Validate.isTrue( + dest.getTransferType() == DataBuffer.TYPE_BYTE && dest.getNumDataElements() >= 3 || + dest.getTransferType() == DataBuffer.TYPE_INT && dest.getNumDataElements() == 1, + src, + "only 3 or 4 byte TYPE_BYTE or 1 int TYPE_INT rasters supported as dest: %s" + ); + } + + final int height = src.getHeight(); + final int width = src.getWidth(); + + final byte[] in = new byte[src.getNumDataElements()]; // CMYK + + if (dest.getTransferType() == DataBuffer.TYPE_BYTE) { + final byte[] out = new byte[dest.getNumDataElements()]; + + if (out.length > 3) { + out[3] = (byte) 0xFF; + } + + for (int y = dest.getMinY(); y < height; y++) { + for (int x = dest.getMinX(); x < width; x++) { + src.getDataElements(x, y, in); + convertCMYKToRGB(in, out); + dest.setDataElements(x, y, out); + } + } + } + else if (dest.getTransferType() == DataBuffer.TYPE_INT) { + final int[] out = new int[dest.getNumDataElements()]; + final byte[] temp = new byte[3]; // RGB + + // Special case for INT_BGR types, as bit offsets are not handled in setDataElements like for the byte raster case + int[] bitOffsets; + SampleModel sm = dest.getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + bitOffsets = ((SinglePixelPackedSampleModel) sm).getBitOffsets(); + } + else { + bitOffsets = new int[]{0, 8, 16}; + } + + final int alpha = bitOffsets.length > 3 ? 0xFF : 0x00; + + for (int y = dest.getMinY(); y < height; y++) { + for (int x = dest.getMinX(); x < width; x++) { + src.getDataElements(x, y, in); + convertCMYKToRGB(in, temp); + out[0] = alpha << 24 | (temp[0] & 0xFF) << bitOffsets[0] | (temp[1] & 0xFF) << bitOffsets[1] | (temp[2] & 0xFF) << bitOffsets[2]; + dest.setDataElements(x, y, out); + } + } + } + else { + // This is already tested for + throw new AssertionError(); + } + + return dest; + } + + @SuppressWarnings({"PointlessArithmeticExpression"}) + private void convertCMYKToRGB(byte[] cmyk, byte[] rgb) { + rgb[0] = (byte) (((255 - cmyk[0] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + rgb[1] = (byte) (((255 - cmyk[1] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + rgb[2] = (byte) (((255 - cmyk[2] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + } + + public Rectangle2D getBounds2D(Raster src) { + return src.getBounds(); + } + + public WritableRaster createCompatibleDestRaster(final Raster src) { + return src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[]{0, 1, 2}).createCompatibleWritableRaster(); + } + + /* + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + Validate.notNull(src, "src may not be null"); +// Validate.isTrue(src != dest, "src and dest image may not be same"); + + if (dest == null) { + dest = createCompatibleDestImage(src, ColorModel.getRGBdefault()); + } + + filter(src.getRaster(), dest.getRaster()); + + return dest; + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return getBounds2D(src.getRaster()); + } + + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + // TODO: dest color model depends on bands... + return destCM == null ? + new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_3BYTE_BGR) : + new BufferedImage(destCM, destCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), destCM.isAlphaPremultiplied(), null); + } + */ + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY()); + } + else { + dstPt.setLocation(srcPt); + } + + return dstPt; + } + + public RenderingHints getRenderingHints() { + return null; + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java new file mode 100644 index 00000000..9f8bb46d --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import org.junit.Test; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * FastCMYKToRGBTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FastCMYKToRGBTest.java,v 1.0 22.02.11 16.22 haraldk Exp$ + */ +public class FastCMYKToRGBTest { + @Test + public void testCreate() { + new FastCMYKToRGB(); + } + + @Test + public void testConvertByteRGBWhite() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = convert.filter(input, null); + byte[] pixel = (byte[]) result.getDataElements(0, 0, null); + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) 255, (byte) 255, (byte) 255}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + + @Test + public void testConvertIntRGBWhite() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = convert.filter(input, new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster()); + int[] pixel = (int[]) result.getDataElements(0, 0, null); + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0xFFFFFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + + @Test + public void testConvertByteRGBBlack() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) 0, (byte) 0, (byte) 0}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertIntRGBBlack() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0x0; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertByteRGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertByteBGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertByteABGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR).getRaster(); + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(4, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i), (byte) 0xff}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertIntRGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = (((byte) (255 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (127 - i)) & 0xFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertIntBGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_BGR).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = (((byte) (127 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (255 - i)) & 0xFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertIntARGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[]{(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0xFF << 24 | (((byte) (255 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (127 - i)) & 0xFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", pixel[0], expected), expected, pixel[0]); + } + } +}