All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twelvemonkeys.image.MagickUtil Maven / Gradle / Ivy

There is a newer version: 1.2.2.1-jre17
Show newest version
/*
 * Copyright (c) 2008, 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 of the copyright holder 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 HOLDER 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.image;

import magick.ImageType;
import magick.MagickException;
import magick.MagickImage;
import magick.PixelPacket;

import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;

/**
 * Utility for converting JMagick {@code MagickImage}s to standard Java
 * {@code BufferedImage}s and back.
 * 

* NOTE: This class is considered an implementation detail and not part of * the public API. This class is subject to change without further notice. * You have been warned. :-) *

* * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java#4 $ */ public final class MagickUtil { // IMPORTANT NOTE: Disaster happens if any of these constants are used outside this class // because you then have a dependency on MagickException (this is due to Java class loading // and initialization magic). // Do not use outside this class. If the constants need to be shared, move to Magick or ImageUtil. /** Color Model usesd for bilevel (B/W) */ private static final IndexColorModel CM_MONOCHROME = MonochromeColorModel.getInstance(); /** Color Model usesd for raw ABGR */ private static final ColorModel CM_COLOR_ALPHA = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8, 8}, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); /** Color Model usesd for raw BGR */ private static final ColorModel CM_COLOR_OPAQUE = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8}, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); /** Color Model usesd for raw RGB */ //private static final ColorModel CM_COLOR_RGB = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x0); /** Color Model usesd for raw GRAY + ALPHA */ private static final ColorModel CM_GRAY_ALPHA = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); /** Color Model usesd for raw GRAY */ private static final ColorModel CM_GRAY_OPAQUE = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); /** Band offsets for raw ABGR */ private static final int[] BAND_OFF_TRANS = new int[] {3, 2, 1, 0}; /** Band offsets for raw BGR */ private static final int[] BAND_OFF_OPAQUE = new int[] {2, 1, 0}; /** The point at {@code 0, 0} */ private static final Point LOCATION_UPPER_LEFT = new Point(0, 0); private static final boolean DEBUG = Magick.DEBUG; // Only static members and methods private MagickUtil() {} /** * Converts a {@code MagickImage} to a {@code BufferedImage}. *

* The conversion depends on {@code pImage}'s {@code ImageType}: *

*
*
{@code ImageType.BilevelType}
*
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY}
* *
{@code ImageType.GrayscaleType}
*
{@code BufferedImage} of type {@code TYPE_BYTE_GRAY}
*
{@code ImageType.GrayscaleMatteType}
*
{@code BufferedImage} of type {@code TYPE_USHORT_GRAY}
* *
{@code ImageType.PaletteType}
*
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
*
{@code ImageType.PaletteMatteType}
*
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
* *
{@code ImageType.TrueColorType}
*
{@code BufferedImage} of type {@code TYPE_3BYTE_BGR}
*
{@code ImageType.TrueColorPaletteType}
*
{@code BufferedImage} of type {@code TYPE_4BYTE_ABGR}
*
* * @param pImage the original {@code MagickImage} * @return a new {@code BufferedImage} * * @throws IllegalArgumentException if {@code pImage} is {@code null} * or if the {@code ImageType} is not one mentioned above. * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ public static BufferedImage toBuffered(MagickImage pImage) throws MagickException { if (pImage == null) { throw new IllegalArgumentException("image == null"); } long start = 0L; if (DEBUG) { start = System.currentTimeMillis(); } BufferedImage image = null; try { switch (pImage.getImageType()) { case ImageType.BilevelType: image = bilevelToBuffered(pImage); break; case ImageType.GrayscaleType: image = grayToBuffered(pImage, false); break; case ImageType.GrayscaleMatteType: image = grayToBuffered(pImage, true); break; case ImageType.PaletteType: image = paletteToBuffered(pImage, false); break; case ImageType.PaletteMatteType: image = paletteToBuffered(pImage, true); break; case ImageType.TrueColorType: image = rgbToBuffered(pImage, false); break; case ImageType.TrueColorMatteType: image = rgbToBuffered(pImage, true); break; case ImageType.ColorSeparationType: image = cmykToBuffered(pImage, false); break; case ImageType.ColorSeparationMatteType: image = cmykToBuffered(pImage, true); break; case ImageType.OptimizeType: default: throw new IllegalArgumentException("Unknown JMagick image type: " + pImage.getImageType()); } } finally { if (DEBUG) { long time = System.currentTimeMillis() - start; System.out.println("Converted JMagick image type: " + pImage.getImageType() + " to BufferedImage: " + image); System.out.println("Conversion to BufferedImage: " + time + " ms"); } } return image; } /** * Converts a {@code BufferedImage} to a {@code MagickImage}. *

* The conversion depends on {@code pImage}'s {@code ColorModel}: *

*
*
{@code IndexColorModel} with 1 bit b/w
*
{@code MagickImage} of type {@code ImageType.BilevelType}
*
{@code IndexColorModel} > 1 bit,
*
{@code MagickImage} of type {@code ImageType.PaletteType} * or {@code MagickImage} of type {@code ImageType.PaletteMatteType} * depending on ColorModel.getAlpha()
* *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY}
*
{@code MagickImage} of type {@code ImageType.GrayscaleType} * or {@code MagickImage} of type {@code ImageType.GrayscaleMatteType} * depending on ColorModel.getAlpha()
* *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_RGB}
*
{@code MagickImage} of type {@code ImageType.TrueColorType} * or {@code MagickImage} of type {@code ImageType.TrueColorPaletteType}
*
* * @param pImage the original {@code BufferedImage} * @return a new {@code MagickImage} * * @throws IllegalArgumentException if {@code pImage} is {@code null} * or if the {@code ColorModel} is not one mentioned above. * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ public static MagickImage toMagick(BufferedImage pImage) throws MagickException { if (pImage == null) { throw new IllegalArgumentException("image == null"); } long start = 0L; if (DEBUG) { start = System.currentTimeMillis(); } try { ColorModel cm = pImage.getColorModel(); if (cm instanceof IndexColorModel) { // Handles both BilevelType, PaletteType and PaletteMatteType return indexedToMagick(pImage, (IndexColorModel) cm, cm.hasAlpha()); } switch (cm.getColorSpace().getType()) { case ColorSpace.TYPE_GRAY: // Handles GrayType and GrayMatteType return grayToMagick(pImage, cm.hasAlpha()); case ColorSpace.TYPE_RGB: // Handles TrueColorType and TrueColorMatteType return rgbToMagic(pImage, cm.hasAlpha()); case ColorSpace.TYPE_CMY: case ColorSpace.TYPE_CMYK: case ColorSpace.TYPE_HLS: case ColorSpace.TYPE_HSV: // Other types not supported yet default: throw new IllegalArgumentException("Unknown buffered image type: " + pImage); } } finally { if (DEBUG) { long time = System.currentTimeMillis() - start; System.out.println("Conversion to MagickImage: " + time + " ms"); } } } private static MagickImage rgbToMagic(BufferedImage pImage, boolean pAlpha) throws MagickException { MagickImage image = new MagickImage(); BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); // Need to get data of sub raster, not the full data array, this is // just a convenient way Raster raster; if (buffered.getRaster().getParent() != null) { raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); } else { raster = buffered.getRaster(); } image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "BGR", ((DataBufferByte) raster.getDataBuffer()).getData()); return image; } private static MagickImage grayToMagick(BufferedImage pImage, boolean pAlpha) throws MagickException { MagickImage image = new MagickImage(); // TODO: Make a fix for TYPE_USHORT_GRAY // The code below does not seem to work (JMagick issues?)... /* if (pImage.getType() == BufferedImage.TYPE_USHORT_GRAY) { short[] data = ((DataBufferUShort) pImage.getRaster().getDataBuffer()).getData(); int[] intData = new int[data.length]; for (int i = 0; i < data.length; i++) { intData[i] = (data[i] & 0xffff) * 0xffff; } image.constituteImage(pImage.getWidth(), pImage.getHeight(), "I", intData); System.out.println("storageClass: " + image.getStorageClass()); System.out.println("depth: " + image.getDepth()); System.out.println("imageType: " + image.getImageType()); } else { */ BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_BYTE_GRAY); // Need to get data of sub raster, not the full data array, this is // just a convenient way Raster raster; if (buffered.getRaster().getParent() != null) { raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); } else { raster = buffered.getRaster(); } image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "I", ((DataBufferByte) raster.getDataBuffer()).getData()); //} return image; } private static MagickImage indexedToMagick(BufferedImage pImage, IndexColorModel pColorModel, boolean pAlpha) throws MagickException { MagickImage image = rgbToMagic(pImage, pAlpha); int mapSize = pColorModel.getMapSize(); image.setNumberColors(mapSize); return image; } /* public static MagickImage toMagick(BufferedImage pImage) throws MagickException { if (pImage == null) { throw new IllegalArgumentException("image == null"); } final int width = pImage.getWidth(); final int height = pImage.getHeight(); // int ARGB -> byte RGBA conversion // NOTE: This is ImageMagick Q16 compatible raw RGBA format with 16 bits/sample... // For a Q8 build, we could probably go with half the space... // NOTE: This is close to insanity, as it wastes extreme ammounts of memory final int[] argb = new int[width]; final byte[] raw16 = new byte[width * height * 8]; for (int y = 0; y < height; y++) { // Fetch one line of ARGB data pImage.getRGB(0, y, width, 1, argb, 0, width); for (int x = 0; x < width; x++) { int pixel = (x + (y * width)) * 8; raw16[pixel ] = (byte) ((argb[x] >> 16) & 0xff); // R raw16[pixel + 2] = (byte) ((argb[x] >> 8) & 0xff); // G raw16[pixel + 4] = (byte) ((argb[x] ) & 0xff); // B raw16[pixel + 6] = (byte) ((argb[x] >> 24) & 0xff); // A } } // Create magick image ImageInfo info = new ImageInfo(); info.setMagick("RGBA"); // Raw RGBA samples info.setSize(width + "x" + height); // String?!? MagickImage image = new MagickImage(info); image.setImageAttribute("depth", "8"); // Set pixel data in 16 bit raw RGBA format image.blobToImage(info, raw16); return image; } */ /** * Converts a bi-level {@code MagickImage} to a {@code BufferedImage}, of * type {@code TYPE_BYTE_BINARY}. * * @param pImage the original {@code MagickImage} * @return a new {@code BufferedImage} * * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ private static BufferedImage bilevelToBuffered(MagickImage pImage) throws MagickException { // As there is no way to get the binary representation of the image, // convert to gray, and the create a binary image from it BufferedImage temp = grayToBuffered(pImage, false); BufferedImage image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, CM_MONOCHROME); ImageUtil.drawOnto(image, temp); return image; } /** * Converts a gray {@code MagickImage} to a {@code BufferedImage}, of * type {@code TYPE_USHORT_GRAY} or {@code TYPE_BYTE_GRAY}. * * @param pImage the original {@code MagickImage} * @param pAlpha keep alpha channel * @return a new {@code BufferedImage} * * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ private static BufferedImage grayToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { Dimension size = pImage.getDimension(); int length = size.width * size.height; int bands = pAlpha ? 2 : 1; byte[] pixels = new byte[length * bands]; // TODO: Make a fix for 16 bit TYPE_USHORT_GRAY?! // Note: The ordering AI or I corresponds to BufferedImage // TYPE_CUSTOM and TYPE_BYTE_GRAY respectively pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "AI" : "I", pixels); // Init databuffer with array, to avoid allocation of empty array DataBuffer buffer = new DataBufferByte(pixels, pixels.length); int[] bandOffsets = pAlpha ? new int[] {1, 0} : new int[] {0}; WritableRaster raster = Raster.createInterleavedRaster(buffer, size.width, size.height, size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); return new BufferedImage(pAlpha ? CM_GRAY_ALPHA : CM_GRAY_OPAQUE, raster, pAlpha, null); } /** * Converts a palette-based {@code MagickImage} to a * {@code BufferedImage}, of type {@code TYPE_BYTE_BINARY} (for images * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}. * * @param pImage the original {@code MagickImage} * @param pAlpha keep alpha channel * @return a new {@code BufferedImage} * * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ private static BufferedImage paletteToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { // Create indexcolormodel for the image IndexColorModel cm; try { cm = createIndexColorModel(pImage.getColormap(), pAlpha); } catch (MagickException e) { // NOTE: Some MagickImages incorrecly (?) reports to be paletteType, // but does not have a colormap, this is a workaround. return rgbToBuffered(pImage, pAlpha); } // As there is no way to get the indexes of an indexed image, convert to // RGB, and the create an indexed image from it BufferedImage temp = rgbToBuffered(pImage, pAlpha); BufferedImage image; if (cm.getMapSize() <= 16) { image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, cm); } else { image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm); } // Create transparent background for images containing alpha if (pAlpha) { Graphics2D g = image.createGraphics(); try { g.setComposite(AlphaComposite.Clear); g.fillRect(0, 0, temp.getWidth(), temp.getHeight()); } finally { g.dispose(); } } // NOTE: This is (surprisingly) much faster than using g2d.drawImage().. // (Tests shows 20-30ms, vs. 600-700ms on the same image) BufferedImageOp op = new CopyDither(cm); op.filter(temp, image); return image; } /** * Creates an {@code IndexColorModel} from an array of * {@code PixelPacket}s. * * @param pColormap the original colormap as a {@code PixelPacket} array * @param pAlpha keep alpha channel * * @return a new {@code IndexColorModel} */ public static IndexColorModel createIndexColorModel(PixelPacket[] pColormap, boolean pAlpha) { int[] colors = new int[pColormap.length]; // TODO: Verify if this is correct for alpha...? int trans = pAlpha ? colors.length - 1 : -1; //for (int i = 0; i < pColormap.length; i++) { for (int i = pColormap.length - 1; i != 0; i--) { PixelPacket color = pColormap[i]; if (pAlpha) { colors[i] = (0xff - (color.getOpacity() & 0xff)) << 24 | (color.getRed() & 0xff) << 16 | (color.getGreen() & 0xff) << 8 | (color.getBlue() & 0xff); } else { colors[i] = (color.getRed() & 0xff) << 16 | (color.getGreen() & 0xff) << 8 | (color.getBlue() & 0xff); } } return new InverseColorMapIndexColorModel(8, colors.length, colors, 0, pAlpha, trans, DataBuffer.TYPE_BYTE); } /** * Converts an (A)RGB {@code MagickImage} to a {@code BufferedImage}, of * type {@code TYPE_4BYTE_ABGR} or {@code TYPE_3BYTE_BGR}. * * @param pImage the original {@code MagickImage} * @param pAlpha keep alpha channel * @return a new {@code BufferedImage} * * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ private static BufferedImage rgbToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { Dimension size = pImage.getDimension(); int length = size.width * size.height; int bands = pAlpha ? 4 : 3; byte[] pixels = new byte[length * bands]; // TODO: If we do multiple dispatches (one per line, typically), we could provide listener // feedback. But it's currently a lot slower than fetching all the pixels in one go. // Note: The ordering ABGR or BGR corresponds to BufferedImage // TYPE_4BYTE_ABGR and TYPE_3BYTE_BGR respectively pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ABGR" : "BGR", pixels); // Init databuffer with array, to avoid allocation of empty array DataBuffer buffer = new DataBufferByte(pixels, pixels.length); int[] bandOffsets = pAlpha ? BAND_OFF_TRANS : BAND_OFF_OPAQUE; WritableRaster raster = Raster.createInterleavedRaster(buffer, size.width, size.height, size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); return new BufferedImage(pAlpha ? CM_COLOR_ALPHA : CM_COLOR_OPAQUE, raster, pAlpha, null); } /** * Converts an {@code MagickImage} to a {@code BufferedImage} which holds an CMYK ICC profile * * @param pImage the original {@code MagickImage} * @param pAlpha keep alpha channel * @return a new {@code BufferedImage} * * @throws MagickException if an exception occurs during conversion * * @see BufferedImage */ private static BufferedImage cmykToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { Dimension size = pImage.getDimension(); int length = size.width * size.height; // Retreive the ICC profile ICC_Profile profile = ICC_Profile.getInstance(pImage.getColorProfile().getInfo()); ColorSpace cs = new ICC_ColorSpace(profile); int bands = cs.getNumComponents() + (pAlpha ? 1 : 0); int[] bits = new int[bands]; for (int i = 0; i < bands; i++) { bits[i] = 8; } ColorModel cm = pAlpha ? new ComponentColorModel(cs, bits, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE) : new ComponentColorModel(cs, bits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); byte[] pixels = new byte[length * bands]; // TODO: If we do multiple dispatches (one per line, typically), we could provide listener // feedback. But it's currently a lot slower than fetching all the pixels in one go. // TODO: handle more generic cases if profile is not CMYK // TODO: Test "ACMYK" pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ACMYK" : "CMYK", pixels); // Init databuffer with array, to avoid allocation of empty array DataBuffer buffer = new DataBufferByte(pixels, pixels.length); // TODO: build array from bands variable, here it just works for CMYK // The values has not been tested with an alpha picture actually... int[] bandOffsets = pAlpha ? new int[] {0, 1, 2, 3, 4} : new int[] {0, 1, 2, 3}; WritableRaster raster = Raster.createInterleavedRaster(buffer, size.width, size.height, size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); return new BufferedImage(cm, raster, pAlpha, null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy