/* * Copyright 2017 Laszlo Balazs-Csiki * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License, version 3 as published by the Free * Software Foundation. * * Pixelitor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>. */ package pixelitor.utils; import com.jhlabs.composite.OverlayComposite; import com.jhlabs.composite.ScreenComposite; import com.jhlabs.image.BoxBlurFilter; import com.jhlabs.image.EmbossFilter; import org.jdesktop.swingx.graphics.BlendComposite; import org.jdesktop.swingx.painter.CheckerboardPainter; import pixelitor.colors.ColorUtils; import pixelitor.filters.Invert; import pixelitor.gui.ImageComponents; import pixelitor.gui.utils.Dialogs; import pixelitor.menus.view.ZoomLevel; import pixelitor.utils.debug.BufferedImageNode; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Transparency; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.PixelGrabber; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.URL; import java.util.Random; import static java.awt.AlphaComposite.SRC_OVER; import static java.awt.Color.BLACK; import static java.awt.Color.WHITE; import static java.awt.RenderingHints.KEY_ANTIALIASING; import static java.awt.RenderingHints.KEY_INTERPOLATION; import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; import static java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; import static java.awt.image.BufferedImage.TYPE_BYTE_GRAY; import static java.awt.image.BufferedImage.TYPE_INT_ARGB; import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE; import static java.awt.image.BufferedImage.TYPE_INT_RGB; /** * Image utility methods */ public class ImageUtils { public static final double DEG_315_IN_RADIANS = 0.7853981634; public static final float[] FRACTIONS_2_COLOR_UNIFORM = {0.0f, 1.0f}; private static final Color CHECKERBOARD_GRAY = new Color(200, 200, 200); /** * Utility class with static methods */ private ImageUtils() { } public static CheckerboardPainter createCheckerboardPainter() { return new CheckerboardPainter(CHECKERBOARD_GRAY, Color.WHITE); } public static BufferedImage toSysCompatibleImage(BufferedImage input) { assert input != null; GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration(); if (input.getColorModel().equals(gc.getColorModel())) { // already compatible return input; } int transparency = Transparency.TRANSLUCENT; BufferedImage output = gc.createCompatibleImage(input.getWidth(), input.getHeight(), transparency); Graphics2D g = output.createGraphics(); g.drawImage(input, 0, 0, null); g.dispose(); return output; } public static BufferedImage createSysCompatibleImage(int width, int height) { assert (width > 0) && (height > 0); GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT); } public static BufferedImage createImageWithSameColorModel(BufferedImage src) { ColorModel dstCM = src.getColorModel(); return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); } // like the above but instead of src width and height, it uses the arguments public static BufferedImage createImageWithSameColorModel(BufferedImage src, int width, int height) { ColorModel dstCM = src.getColorModel(); return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(width, height), dstCM.isAlphaPremultiplied(), null); } // From the Filthy Rich Clients book /** * Convenience method that returns a scaled instance of the * provided BufferedImage. * * @param img the original image to be scaled * @param targetWidth the desired width of the scaled instance, * in pixels * @param targetHeight the desired height of the scaled instance, * in pixels * @param hint one of the rendering hints that corresponds to * RenderingHints.KEY_INTERPOLATION (e.g. * RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR, * RenderingHints.VALUE_INTERPOLATION_BILINEAR, * RenderingHints.VALUE_INTERPOLATION_BICUBIC) * @param progressiveBilinear if true, this method will use a multi-step * scaling technique that provides higher quality than the usual * one-step technique (only useful in down-scaling cases, where * targetWidth or targetHeight is * smaller than the original dimensions) * @return a scaled version of the original BufferedImage */ public static BufferedImage getFasterScaledInstance(BufferedImage img, int targetWidth, int targetHeight, Object hint, boolean progressiveBilinear) { assert img != null; int prevW = img.getWidth(); int prevH = img.getHeight(); if (targetWidth >= prevW || targetHeight >= prevH) { progressiveBilinear = false; } // // TODO in these two cases the original method from the Filthy Rich Clients book goes into infinite loop! // if ((prevH <= targetHeight) && (prevW >= targetWidth)) { // return simpleResize(img, targetWidth, targetHeight, hint); // } // if ((prevH > targetHeight) && (prevW < targetWidth)) { // return simpleResize(img, targetWidth, targetHeight, hint); // } // int type = (img.getTransparency() == Transparency.OPAQUE) ? // BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; int type = img.getType(); BufferedImage ret = img; BufferedImage scratchImage = null; Graphics2D g2 = null; int w, h; boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; if (progressiveBilinear) { // Use multi-step technique: start with original size, then // scale down in multiple passes with drawImage() // until the target size is reached w = img.getWidth(); h = img.getHeight(); } else { // Use one-step technique: scale directly from original // size to target size with a single drawImage() call w = targetWidth; h = targetHeight; } do { if (progressiveBilinear && (w > targetWidth)) { w /= 2; if (w < targetWidth) { w = targetWidth; } } if (progressiveBilinear && (h > targetHeight)) { h /= 2; if (h < targetHeight) { h = targetHeight; } } if ((scratchImage == null) || isTranslucent) { // Use a single scratch buffer for all iterations // and then copy to the final, correctly-sized image // before returning scratchImage = new BufferedImage(w, h, type); g2 = scratchImage.createGraphics(); } g2.setRenderingHint(KEY_INTERPOLATION, hint); g2.drawImage(ret, 0, 0, w, h, 0, 0, prevW, prevH, null); prevW = w; prevH = h; ret = scratchImage; } while ((w != targetWidth) || (h != targetHeight)); if (g2 != null) { g2.dispose(); } // If we used a scratch buffer that is larger than our target size, // create an image of the right size and copy the results into it if ((targetWidth != ret.getWidth()) || (targetHeight != ret.getHeight())) { scratchImage = new BufferedImage(targetWidth, targetHeight, type); g2 = scratchImage.createGraphics(); g2.drawImage(ret, 0, 0, null); g2.dispose(); ret = scratchImage; } return ret; } /** * Also an iterative approach, but using even smaller steps */ public static BufferedImage enlargeSmooth(BufferedImage src, int targetWidth, int targetHeight, Object hint, double step, ProgressTracker pt) { int srcWidth = src.getWidth(); int srcHeight = src.getHeight(); double factorX = targetWidth / (double) srcWidth; double factorY = targetHeight / (double) srcHeight; // they should be the same, but rounding errors can cause small problems assert Math.abs(factorX - factorY) < 0.05; double factor = (factorX + factorY) / 2.0; assert factor > 1.0; // this only makes sense for enlarging double progress = 1.0; double lastStep = factor / step; BufferedImage last = src; AffineTransform stepScale = AffineTransform.getScaleInstance(step, step); while (progress < lastStep) { progress = progress * step; int newSrcWidth = (int) (srcWidth * progress); int newSrcHeight = (int) (srcHeight * progress); BufferedImage tmp = new BufferedImage(newSrcWidth, newSrcHeight, src.getType()); Graphics2D g = tmp.createGraphics(); if (hint != null) { g.setRenderingHint(KEY_INTERPOLATION, hint); } g.drawImage(last, stepScale, null); g.dispose(); BufferedImage willBeForgotten = last; last = tmp; willBeForgotten.flush(); pt.unitDone(); } // do the last step: resize exactly to the target values BufferedImage retVal = new BufferedImage(targetWidth, targetHeight, src.getType()); Graphics2D g = retVal.createGraphics(); if (hint != null) { g.setRenderingHint(KEY_INTERPOLATION, hint); } g.drawImage(last, 0, 0, targetWidth, targetHeight, null); g.dispose(); pt.unitDone(); return retVal; } /** * Returns the number of steps necessary for * For progress tracking */ public static int getNumStepsForEnlargeSmooth(double resizeFactor, double step) { double progress = 1.0; double lastStep = resizeFactor / step; int retVal = 1; // for the final step while (progress < lastStep) { progress = progress * step; retVal++; } return retVal; } private static BufferedImage simpleResize(BufferedImage img, int targetWidth, int targetHeight, Object hint) { assert img != null; BufferedImage ret = new BufferedImage(targetWidth, targetHeight, img.getType()); Graphics2D g2 = ret.createGraphics(); g2.setRenderingHint(KEY_INTERPOLATION, hint); g2.drawImage(img, 0, 0, targetWidth, targetHeight, null); g2.dispose(); return ret; } // TODO duplicate functionality public static BufferedImage resizeImage(double newSize, BufferedImage original) { int originalWidth = original.getWidth(); int originalHeight = original.getHeight(); int maxOriginalSize = Math.max(originalWidth, originalHeight); double ratio = ((double) maxOriginalSize) / newSize; int imageWidth = (int) (originalWidth / ratio); int imageHeight = (int) (originalHeight / ratio); BufferedImage resizedImage = new BufferedImage(imageWidth, imageHeight, TYPE_INT_ARGB_PRE); Graphics2D g2 = resizedImage.createGraphics(); g2.drawImage(original, 0, 0, imageWidth, imageHeight, null); g2.dispose(); return resizedImage; } /** * Samples 9 pixels at and around the given pixel coordinates * * @param src * @param x * @param y * @return the average color */ public static Color sample9Points(BufferedImage src, int x, int y) { int averageRed = 0; int averageGreen = 0; int averageBlue = 0; int width = src.getWidth(); int height = src.getHeight(); for (int i = x - 1; i < x + 2; i++) { for (int j = y - 1; j < y + 2; j++) { int limitedX = limitSamplingIndex(i, width - 1); int limitedY = limitSamplingIndex(j, height - 1); int rgb = src.getRGB(limitedX, limitedY); // int a = (rgb >>> 24) & 0xFF; int r = (rgb >>> 16) & 0xFF; int g = (rgb >>> 8) & 0xFF; int b = (rgb) & 0xFF; averageRed += r; averageGreen += g; averageBlue += b; } } return new Color(averageRed / 9, averageGreen / 9, averageBlue / 9); } private static int limitSamplingIndex(int x, int max) { int r = x; if (r < 0) { r = 0; } if (r > max) { r = max; } return r; } public static boolean hasPackedIntArray(BufferedImage image) { assert image != null; int type = image.getType(); return (type == TYPE_INT_ARGB_PRE || type == TYPE_INT_RGB || type == TYPE_INT_ARGB); } /** * This methods returns the pixel array behind the given BufferedImage * If the array data is modified, the image itself is modified */ public static int[] getPixelsAsArray(BufferedImage src) { assert src != null; int[] pixels; boolean fastWay = hasPackedIntArray(src); if (fastWay) { DataBufferInt srcDataBuffer = (DataBufferInt) src.getRaster().getDataBuffer(); pixels = srcDataBuffer.getData(); } else if (src.getType() == BufferedImage.TYPE_BYTE_GRAY) { // TODO this does not seem to work - why? int width = src.getWidth(); int height = src.getHeight(); pixels = src.getRGB(0, 0, width, height, null, 0, width); } else { int width = src.getWidth(); int height = src.getHeight(); pixels = new int[width * height]; PixelGrabber pg = new PixelGrabber(src, 0, 0, width, height, pixels, 0, width); try { pg.grabPixels(); } catch (InterruptedException e) { Messages.showException(e); } } return pixels; } public static byte[] getPixelsAsByteArray(BufferedImage src) { assert src.getType() == BufferedImage.TYPE_BYTE_GRAY; WritableRaster raster = src.getRaster(); DataBufferByte db = (DataBufferByte) raster.getDataBuffer(); return db.getData(); } public static URL resourcePathToURL(String fileName) { assert fileName != null; String iconPath = "/images/" + fileName; URL imgURL = ImageUtils.class.getResource(iconPath); if (imgURL == null) { String message = iconPath + " not found"; Messages.showError("Error", message); } return imgURL; } /** * Loads an image from the images folder */ public static BufferedImage loadBufferedImage(String fileName) { // consider caching // for image brushes this is not necessary because the template brush always has the max size assert fileName != null; URL imgURL = resourcePathToURL(fileName); BufferedImage image = null; try { image = ImageIO.read(imgURL); } catch (IOException e) { Messages.showException(e); } return image; } public static BufferedImage convertToARGB_PRE(BufferedImage src, boolean oldCanBeFlushed) { assert src != null; BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), TYPE_INT_ARGB_PRE); Graphics2D g = dest.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); if (oldCanBeFlushed) { src.flush(); } return dest; } public static BufferedImage convertToARGB(BufferedImage src, boolean oldCanBeFlushed) { assert src != null; BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), TYPE_INT_ARGB); Graphics2D g = dest.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); if (oldCanBeFlushed) { src.flush(); } return dest; } public static BufferedImage convertToRGB(BufferedImage src, boolean oldCanBeFlushed) { assert src != null; BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), TYPE_INT_RGB); Graphics2D g = dest.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); if (oldCanBeFlushed) { src.flush(); } return dest; } // without this the drawing on large images would be very slow // TODO is this faster? the simple g.drawImage also respects the clipping public static void drawImageWithClipping(Graphics g, BufferedImage img) { assert img != null; Rectangle clipBounds = g.getClipBounds(); int clipX = (int) clipBounds.getX(); int clipY = (int) clipBounds.getY(); int clipWidth = (int) clipBounds.getWidth(); int clipHeight = (int) clipBounds.getHeight(); int clipX2 = clipX + clipWidth; int clipY2 = clipY + clipHeight; g.drawImage(img, clipX, clipY, clipX2, clipY2, clipX, clipY, clipX2, clipY2, null); } public static void serializeImage(ObjectOutputStream out, BufferedImage img) throws IOException { assert img != null; int imgType = img.getType(); int imgWidth = img.getWidth(); int imgHeight = img.getHeight(); out.writeInt(imgWidth); out.writeInt(imgHeight); out.writeInt(imgType); if (imgType == BufferedImage.TYPE_BYTE_GRAY) { ImageIO.write(img, "PNG", out); } else { int[] pixelsAsArray = getPixelsAsArray(img); for (int pixel : pixelsAsArray) { out.writeInt(pixel); } } } public static BufferedImage deserializeImage(ObjectInputStream in) throws IOException { int width = in.readInt(); int height = in.readInt(); int type = in.readInt(); if (type == BufferedImage.TYPE_BYTE_GRAY) { return ImageIO.read(in); } else { BufferedImage img = new BufferedImage(width, height, type); int[] pixelsAsArray = getPixelsAsArray(img); for (int i = 0; i < pixelsAsArray.length; i++) { pixelsAsArray[i] = in.readInt(); } return img; } } public static BufferedImage createThumbnail(BufferedImage src, int size, CheckerboardPainter painter) { assert src != null; Dimension thumbDim = calcThumbDimensions(src, size); return downSizeFast(src, painter, thumbDim.width, thumbDim.height); } public static Dimension calcThumbDimensions(BufferedImage src, int size) { int width = src.getWidth(); int height = src.getHeight(); int thumbWidth; int thumbHeight; if (width > height) { thumbWidth = size; float ratio = (float) width / (float) height; thumbHeight = (int) (size / ratio); } else { thumbHeight = size; float ratio = (float) height / (float) width; thumbWidth = (int) (size / ratio); } if (thumbWidth == 0) { thumbWidth = 1; } if (thumbHeight == 0) { thumbHeight = 1; } return new Dimension(thumbWidth, thumbHeight); } public static BufferedImage createThumbnail(BufferedImage src, int maxWidth, int maxHeight, CheckerboardPainter painter) { assert src != null; int imgWidth = src.getWidth(); int imgHeight = src.getHeight(); double xScaling = maxWidth / (double) imgWidth; double yScaling = maxHeight / (double) imgHeight; double scaling = Math.min(xScaling, yScaling); int thumbWidth = (int) (imgWidth * scaling); int thumbHeight = (int) (imgHeight * scaling); return downSizeFast(src, painter, thumbWidth, thumbHeight); } private static BufferedImage downSizeFast(BufferedImage src, CheckerboardPainter painter, int thumbWidth, int thumbHeight) { BufferedImage thumb = createSysCompatibleImage(thumbWidth, thumbHeight); Graphics2D g = thumb.createGraphics(); if(painter != null) { painter.paint(g, null, thumbWidth, thumbHeight); } g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_NEAREST_NEIGHBOR); g.drawImage(src, 0, 0, thumbWidth, thumbHeight, null); g.dispose(); return thumb; } public static void paintRedXOnThumb(BufferedImage thumb) { int thumbWidth = thumb.getWidth(); int thumbHeight = thumb.getHeight(); Graphics2D g = thumb.createGraphics(); g.setColor(new Color(200, 0, 0)); g.setStroke(new BasicStroke(2.5f)); g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); g.drawLine(0, 0, thumbWidth, thumbHeight); g.drawLine(thumbWidth - 1, 0, 0, thumbHeight - 1); g.dispose(); } public static BufferedImage copyImage(BufferedImage src) { assert src != null; WritableRaster raster = null; try { raster = src.copyData(null); } catch (OutOfMemoryError e) { Dialogs.showOutOfMemoryDialog(e); } return new BufferedImage(src.getColorModel(), raster, src.isAlphaPremultiplied(), null); } /** * In contrast to BufferedImage.getSubimage, this method creates a copy of the data */ public static BufferedImage getCopiedSubimage(BufferedImage src, Rectangle bounds) { assert src != null; assert bounds != null; Rectangle intersection = SwingUtilities.computeIntersection( 0, 0, src.getWidth(), src.getHeight(), // image bounds bounds ); if (intersection.width <= 0 || intersection.height <= 0) { throw new IllegalStateException("empty intersection: bounds = " + bounds + ", src width = " + src.getWidth() + ", src height = " + src.getHeight() + ", intersection = " + intersection); } Raster copyRaster = src.getData(intersection); // a copy Raster startingFrom00 = copyRaster.createChild(intersection.x, intersection.y, intersection.width, intersection.height, 0, 0, null); return new BufferedImage(src.getColorModel(), (WritableRaster) startingFrom00, src.isAlphaPremultiplied(), null); } /** * A hack so that Fade can work with PartialImageEdit rasters. * It would be better if Fade could work with rasters directly. */ public static BufferedImage rasterToImage(Raster raster) { assert raster != null; int minX = raster.getMinX(); int minY = raster.getMinY(); int width = raster.getWidth(); int height = raster.getHeight(); Raster startingFrom00 = raster.createChild(minX, minY, width, height, 0, 0, null); BufferedImage image = new BufferedImage(width, height, TYPE_INT_ARGB_PRE); image.setData(startingFrom00); return image; } public static BufferedImage crop(BufferedImage input, int x, int y, int width, int height) { assert input != null; if (width <= 0) { throw new IllegalArgumentException("width = " + width); } if (height <= 0) { throw new IllegalArgumentException("height = " + height); } BufferedImage output = new BufferedImage(width , height , input.getType()); Graphics2D g = output.createGraphics(); AffineTransform t = AffineTransform.getTranslateInstance(-x, -y); g.transform(t); g.drawImage(input, null, 0, 0); g.dispose(); return output; } public static int lerpAndPremultiplyColorWithAlpha(float t, int[] color1, int[] color2) { int alpha = color1[0] + (int) (t * (color2[0] - color1[0])); int red; int green; int blue; if (alpha == 0) { red = 0; green = 0; blue = 0; } else { red = color1[1] + (int) (t * (color2[1] - color1[1])); green = color1[2] + (int) (t * (color2[2] - color1[2])); blue = color1[3] + (int) (t * (color2[3] - color1[3])); if (alpha != 255) { // premultiply float f = alpha / 255.0f; red *= f; green *= f; blue *= f; } } return (alpha << 24 | red << 16 | green << 8 | blue); } public static void screenWithItself(BufferedImage src, float opacity) { assert src != null; Graphics2D g = src.createGraphics(); g.setComposite(new ScreenComposite(opacity)); g.drawImage(src, 0, 0, null); g.dispose(); } public static BufferedImage getHighPassSharpenedImage(BufferedImage original, BufferedImage blurred) { assert original != null; assert blurred != null; // the blurred image is the low-pass filtered version of the image // so we subtract it form the original by inverting it... Invert.invertImage(blurred, blurred); // ... and blending it at 50% with the original Graphics2D g = blurred.createGraphics(); g.setComposite(AlphaComposite.getInstance(SRC_OVER, 0.5f)); g.drawImage(original, 0, 0, null); g.dispose(); // blend it with overlay to get a sharpening effect Graphics2D g2 = blurred.createGraphics(); g2.setComposite(new OverlayComposite(1.0f)); g2.drawImage(original, 0, 0, null); g2.dispose(); return blurred; } public static BufferedImage createRandomPointsTemplateBrush(int diameter, float density) { if (density < 0.0 && density > 1.0) { throw new IllegalArgumentException("density is " + density); } BufferedImage brushImage = new BufferedImage(diameter, diameter, TYPE_INT_ARGB); int radius = diameter / 2; int radius2 = radius * radius; Random random = new Random(); int[] pixels = ImageUtils.getPixelsAsArray(brushImage); for (int x = 0; x < diameter; x++) { for (int y = 0; y < diameter; y++) { int dx = x - radius; int dy = y - radius; int centerDistance2 = dx * dx + dy * dy; if (centerDistance2 < radius2) { float rn = random.nextFloat(); if (density > rn) { pixels[x + y * diameter] = random.nextInt(); } else { pixels[x + y * diameter] = 0xFFFFFFFF; // white } } else { pixels[x + y * diameter] = 0xFFFFFFFF; // white } } } // Fill.fillImage(brushImage, Color.BLACK); return brushImage; } public static BufferedImage createSoftBWBrush(int size) { BufferedImage brushImage = new BufferedImage(size, size, TYPE_INT_ARGB); Graphics2D g = brushImage.createGraphics(); g.setColor(WHITE); g.fillRect(0, 0, size, size); g.setColor(BLACK); int softness = size / 4; g.fillOval(softness, softness, size - 2 * softness, size - 2 * softness); g.dispose(); BoxBlurFilter blur = new BoxBlurFilter(softness, softness, 1, null); brushImage = blur.filter(brushImage, brushImage); return brushImage; } public static BufferedImage createSoftTransparencyImage(int size) { BufferedImage image = createSoftBWBrush(size); int[] pixels = ImageUtils.getPixelsAsArray(image); for (int i = 0, pixelsLength = pixels.length; i < pixelsLength; i++) { int pixelValue = pixels[i] & 0xFF; // take the blue channel: they are all the same int alpha = 255 - pixelValue; pixels[i] = alpha << 24; } return image; } public static BufferedImage getGridImageOnTransparentBackground(Color color, int maxX, int maxY, int hWidth, int hSpacing, int vWidth, int vSpacing, boolean emptyIntersections) { // create transparent image BufferedImage img = new BufferedImage(maxX, maxY, TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); drawGrid(color, g, maxX, maxY, hWidth, hSpacing, vWidth, vSpacing, emptyIntersections); g.dispose(); return img; } public static void drawGrid(Color color, Graphics2D g, int maxX, int maxY, int hWidth, int hSpacing, int vWidth, int vSpacing, boolean emptyIntersections) { if (hWidth < 0) { throw new IllegalArgumentException("hWidth = " + hWidth); } if (vWidth < 0) { throw new IllegalArgumentException("vWidth = " + vWidth); } if (hSpacing <= 0) { throw new IllegalArgumentException("hSpacing = " + hSpacing); } if (vSpacing <= 0) { throw new IllegalArgumentException("vSpacing = " + vSpacing); } g.setColor(color); Composite savedComposite = g.getComposite(); if (emptyIntersections) { g.setComposite(AlphaComposite.Xor); } int halfHWidth = hWidth / 2; int halfVWidth = vWidth / 2; // horizontal lines if (hWidth > 0) { for (int y = 0; y < maxY; y += vSpacing) { int startY = y - halfVWidth; g.fillRect(0, startY, maxX, vWidth); } } // vertical lines if (vWidth > 0) { for (int x = 0; x < maxX; x += hSpacing) { g.fillRect(x - halfHWidth, 0, hWidth, maxY); } } if (emptyIntersections) { g.setComposite(savedComposite); } } public static void drawBrickGrid(Color color, Graphics2D g, int size, int maxX, int maxY) { if (size < 1) { throw new IllegalArgumentException("size = " + size); } g.setColor(color); int doubleSize = size * 2; int y = size; int verticalCount = 0; while (y < maxY) { // vertical lines int hShift = 0; if ((verticalCount % 2) == 1) { hShift = size; } for (int x = hShift; x < maxX; x += doubleSize) { g.drawLine(x, y, x, y - size); } // horizontal lines g.drawLine(0, y, maxX, y); y += size; verticalCount++; } } public static BufferedImage bumpMap(BufferedImage src, BufferedImage bumpMapSource, String filterName) { return bumpMap(src, bumpMapSource, (float) ImageUtils.DEG_315_IN_RADIANS, 0.53f, 2.0f, filterName); } public static BufferedImage bumpMap(BufferedImage src, BufferedImage bumpMapSource, float azimuth, float elevation, float bumpHeight, String filterName) { return bumpMap(src, bumpMapSource, BlendComposite.HardLight, azimuth, elevation, bumpHeight, filterName); } public static BufferedImage bumpMap(BufferedImage src, BufferedImage bumpMapSource, Composite composite, float azimuth, float elevation, float bumpHeight, String filterName) { // TODO optimize it so that the bumpMapSource can be smaller, and an offset is given - useful for text effects // tiling could be also an option EmbossFilter embossFilter = new EmbossFilter(filterName); embossFilter.setAzimuth(azimuth); embossFilter.setElevation(elevation); embossFilter.setBumpHeight(bumpHeight); BufferedImage bumpMap = embossFilter.filter(bumpMapSource, null); BufferedImage dest = ImageUtils.copyImage(src); Graphics2D g = dest.createGraphics(); g.setComposite(composite); g.drawImage(bumpMap, 0, 0, null); g.dispose(); return dest; } /** * Fills the BufferedImage with the specified color */ public static void fillImage(BufferedImage img, Color c) { int[] pixels = getPixelsAsArray(img); int fillColor = c.getRGB(); // int red = c.getRed(); // int green = c.getGreen(); // int blue = c.getBlue(); // // int fillColor = (0xFF000000 | (red << 16) | (green << 8) | blue); for (int i = 0; i < pixels.length; i++) { pixels[i] = fillColor; } } public static int premultiply(int rgb) { int a = (rgb >>> 24) & 0xFF; int r = (rgb >>> 16) & 0xFF; int g = (rgb >>> 8) & 0xFF; int b = rgb & 0xFF; float f = a * (1.0f / 255.0f); r *= f; g *= f; b *= f; return (a << 24) | (r << 16) | (g << 8) | b; } public static int unPremultiply(int rgb) { int a = (rgb >>> 24) & 0xFF; int r = (rgb >>> 16) & 0xFF; int g = (rgb >>> 8) & 0xFF; int b = rgb & 0xFF; if (a == 0 || a == 255) { return rgb; } float f = 255.0f / a; r *= f; g *= f; b *= f; if (r > 255) { r = 255; } if (g > 255) { g = 255; } if (b > 255) { b = 255; } return (a << 24) | (r << 16) | (g << 8) | b; } // // TODO this method increases the contrast of the image - why? // public static BufferedImage convertToGrayScaleImage(BufferedImage src) { // BufferedImage dest = new BufferedImage(src.getWidth(), // src.getHeight(), // TYPE_BYTE_GRAY); // ColorConvertOp colorConvertOp = new ColorConvertOp(null); // dest = colorConvertOp.filter(src, dest); // return dest; // } public static BufferedImage convertToGrayScaleImage(BufferedImage src) { BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), TYPE_BYTE_GRAY); Graphics2D g2 = dest.createGraphics(); g2.drawImage(src, 0, 0, null); g2.dispose(); return dest; } public static void paintAffectedAreaShapes(BufferedImage image, Shape[] shapes) { Graphics2D g = image.createGraphics(); g.setColor(BLACK); ZoomLevel zoomLevel = ImageComponents.getActiveIC().getZoomLevel(); // g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setStroke(zoomLevel.getOuterGeometryStroke()); for (Shape shape : shapes) { g.draw(shape); } g.setColor(WHITE); g.setStroke(zoomLevel.getInnerGeometryStroke()); for (Shape shape : shapes) { g.draw(shape); } g.dispose(); } public static void debugImageToText(BufferedImage img) { BufferedImageNode imgNode = new BufferedImageNode("debug", img); String s = imgNode.toDetailedString(); System.out.println(String.format("ImageUtils::debugImage: s = '%s'", s)); } public static void fillWithTransparentRectangle(Graphics2D g, int size) { g.setComposite(AlphaComposite.Clear); g.fillRect(0, 0, size, size); g.setComposite(AlphaComposite.SrcOver); } public static boolean compareSmallImages(BufferedImage img1, BufferedImage img2) { assert img1.getWidth() == img2.getWidth(); assert img1.getHeight() == img2.getHeight(); int width = img1.getWidth(); int height = img1.getHeight(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb1 = img1.getRGB(x, y); int rgb2 = img2.getRGB(x, y); if (rgb1 != rgb2) { String msg = String.format("at (%d, %d) rgb1 is %s and rgb2 is %s", x, y, ColorUtils.intColorToString(rgb1), ColorUtils.intColorToString(rgb2)); System.out.println(String.format("ImageUtils::compareSmallImages: %s", msg)); return false; } } } return true; } public static String debugSmallImage(BufferedImage im) { int width = im.getWidth(); int height = im.getHeight(); StringBuilder s = new StringBuilder(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb = im.getRGB(x, y); String asString = ColorUtils.intColorToString(rgb); s.append(asString); if(x == width - 1) { s.append("\n"); } else { s.append(" "); } } } return s.toString(); } public static BufferedImage create1x1Image(Color c) { return create1x1Image(c.getAlpha(), c.getRed(), c.getGreen(), c.getBlue()); } public static BufferedImage create1x1Image(int a, int r, int g, int b) { BufferedImage img = createSysCompatibleImage(1, 1); img.setRGB(0, 0, ColorUtils.toPackedInt(a, r, g, b)); return img; } }