001package arcommon.image;
002
003import java.awt.Component;
004import java.awt.Cursor;
005import java.awt.Graphics;
006import java.awt.Image;
007import java.awt.Point;
008import java.awt.Rectangle;
009import java.awt.Toolkit;
010import java.awt.image.BufferedImage;
011import java.awt.image.ImageObserver;
012import java.awt.image.PixelGrabber;
013import java.awt.image.WritableRaster;
014import java.io.ByteArrayInputStream;
015import java.io.ByteArrayOutputStream;
016import java.io.File;
017import java.io.IOException;
018import java.text.DecimalFormat;
019import java.util.Arrays;
020
021import javax.imageio.IIOException;
022import javax.imageio.ImageIO;
023import javax.imageio.ImageReader;
024import javax.imageio.metadata.IIOMetadata;
025import javax.imageio.metadata.IIOMetadataNode;
026import javax.imageio.stream.ImageInputStream;
027
028import org.w3c.dom.Node;
029
030import arcommon.FileUtil;
031import arcommon.StringUtil;
032import arcommon.Util;
033
034public class ImageUtil {
035    public static int rgbToInt(int r, int g, int b) {
036        return (r << 16) + (g << 8) + b;
037    }
038    
039    public static class RGB {
040        public RGB() {
041            ;
042        }
043        
044        public RGB(int pred, int pgreen, int pblue) {
045            this.red = pred;
046            this.green = pgreen;
047            this.blue = pblue;
048            if (red < 0 || red > 255) {
049                throw new RuntimeException("red < 0 || red > 255 " + red);
050            }
051            if (green < 0 || green > 255) {
052                throw new RuntimeException("green < 0 || green > 255 " + green);
053            }
054            if (blue < 0 || blue > 255) {
055                throw new RuntimeException("blue < 0 || blue > 255 " + blue);
056            }
057        }
058
059        public int red;
060        public int green;
061        public int blue;
062        
063        @Override
064        public String toString() {
065            return "(" + red + ", " + green + ", " + blue + ")";
066        }
067    }
068
069    public static RGB intToRgb(int pixel) {
070        RGB result = new RGB();
071//      int alpha = (pixel >> 24) & 0xff;
072        result.red   = (pixel >> 16) & 0xff;
073        result.green = (pixel >>  8) & 0xff;
074        result.blue  = (pixel      ) & 0xff;
075        return result;
076    }
077    
078    private static DecimalFormat decimalFormat1_2 = new DecimalFormat("0.00");
079    private static DecimalFormat decimalFormat3_1 = new DecimalFormat("000.0");
080    
081    public static class HCL {
082        public double hue;
083        public double chroma;
084        public double lightness;
085        
086        @Override
087        public String toString() {
088            return "(hue=" + decimalFormat3_1.format(hue) + ", chroma=" + decimalFormat1_2.format(chroma) + ", lightness=" + decimalFormat1_2.format(lightness);
089        }
090    }
091    
092    public static final double HUE_UNDEFINED = -1.0;
093    
094    public static HCL rgbToHcl(int rInt, int gInt, int bInt) {
095        // See https://en.wikipedia.org/wiki/HSV_color_space
096        double r = (double)rInt / 255;
097        double g = (double)gInt / 255;
098        double b = (double)bInt / 255;
099        double maximum = Math.max(r, Math.max(g, b));
100        double min = Math.min(r, Math.min(g, b));
101        HCL hcl = new HCL();
102        hcl.chroma = maximum - min;
103        hcl.lightness = 0.5 * (maximum + min);
104        if (hcl.chroma == 0.0D) {
105            hcl.hue = HUE_UNDEFINED;
106            return hcl;
107        }
108        double hueMark;
109        if (maximum == r) {
110            hueMark = (g - b) / hcl.chroma % 6;
111        } else {
112            if (maximum == g) {
113                hueMark = (b - r) / hcl.chroma + 2;
114            } else {
115                hueMark = (r - g) / hcl.chroma + 4;
116            }
117        }
118        hcl.hue = (hueMark * 60 + 360) % 360;
119        return hcl;
120    }
121
122    public static RGB hclToRgb(HCL hcl) {
123        if (hcl.hue != HUE_UNDEFINED && (hcl.hue < 0.0D || hcl.hue > 360.0D)) {
124            throw new RuntimeException("hcl.hue < 0.0D || hcl.hue > 360.0D   " + hcl.hue);
125        }
126        if (hcl.chroma < 0.0D || hcl.chroma > 1.0D) {
127            throw new RuntimeException("hcl.chroma < 0.0D || hcl.chroma > 1.0D   " + hcl.chroma);
128        }
129        if (hcl.lightness < 0.0D || hcl.lightness > 1.0D) {
130            throw new RuntimeException("hcl.lightness < 0.0D || hcl.lightness > 1.0D   " + hcl.lightness);
131        }
132        double r1, g1, b1;
133        if (hcl.hue == HUE_UNDEFINED) {
134            r1 = 0;
135            g1 = 0;
136            b1 = 0;
137        } else {
138            double hueMark = hcl.hue / 60;
139            double x = hcl.chroma * (1 - Math.abs(hueMark % 2 - 1));
140            if (hueMark < 0.0D) {
141                throw new RuntimeException("Wrong hue " + hcl.hue);
142            } else if (hueMark < 1.0D) {
143                r1 = hcl.chroma;
144                g1 = x;
145                b1 = 0;
146            } else if (hueMark < 2.0D) {
147                r1 = x;
148                g1 = hcl.chroma;
149                b1 = 0;
150            } else if (hueMark < 3.0D) {
151                r1 = 0;
152                g1 = hcl.chroma;
153                b1 = x;
154            } else if (hueMark < 4.0D) {
155                r1 = 0;
156                g1 = x;
157                b1 = hcl.chroma;
158            } else if (hueMark < 5.0D) {
159                r1 = x;
160                g1 = 0;
161                b1 = hcl.chroma;
162            } else if (hueMark < 6.0D) {
163                r1 = hcl.chroma;
164                g1 = 0;
165                b1 = x;
166            } else {
167                throw new RuntimeException("Wrong hue " + hcl.hue);
168            }
169        }
170        double m = hcl.lightness - 0.5 * hcl.chroma;
171        int r = Math.min((int)((r1 + m) * 255), 255);
172        int g = Math.min((int)((g1 + m) * 255), 255);
173        int b = Math.min((int)((b1 + m) * 255), 255);
174        return new RGB(r, g, b);
175    }
176    
177    public static boolean rgbEquals(int rgb1, int rgb2) {
178        return (rgb1 & 0xffffff) == (rgb2 & 0xffffff);
179    }
180    
181    public static boolean compareImages(BufferedImage bufferedImage1, BufferedImage bufferedImage2) {
182        if (bufferedImage1.getWidth() != bufferedImage2.getWidth() || bufferedImage1.getHeight() != bufferedImage2.getHeight()) {
183            return false;
184        }
185        int[] pixels1 = imageToPixelArray(bufferedImage1);
186        int[] pixels2 = imageToPixelArray(bufferedImage2);
187        return Arrays.equals(pixels1, pixels2);
188    }
189    
190    public static int[] imageToPixelArray(BufferedImage bufferedImage) {
191        int x = 0;
192        int y = 0;
193        int w = bufferedImage.getWidth();
194        int h = bufferedImage.getHeight();
195        int[] pixels = new int[w * h];
196        PixelGrabber pixelGrabber = new PixelGrabber(bufferedImage, x, y, w, h, pixels, 0, w);
197        try {
198            pixelGrabber.grabPixels();
199        } catch (InterruptedException e) {
200            System.err.println("interrupted waiting for pixels!");
201            throw new RuntimeException(e);
202        }
203        if ((pixelGrabber.getStatus() & ImageObserver.ABORT) != 0) {
204            System.err.println("Image fetch aborted or failed");
205            throw new RuntimeException("image fetch aborted or failed");
206        }
207        return pixels;
208    }
209    
210    public static BufferedImage pixelArrayToImage(int[] pixels, int imageWidth, int imageHeight) {
211        int[] tempArray = new int[pixels.length * 3];
212        int index = 0;
213        for (int i = 0; i < pixels.length; i++) {
214            int pixel = pixels[i];
215            RGB rgb = intToRgb(pixel);
216            tempArray[index++] = rgb.red;
217            tempArray[index++] = rgb.green;
218            tempArray[index++] = rgb.blue;
219        }
220
221        BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
222        WritableRaster raster = image.getRaster();
223        raster.setPixels(0, 0, imageWidth, imageHeight, tempArray);
224        
225        return image;
226    }
227
228    public static final int DEFAULT_DELAY = 75;
229    
230    public static class AnimationData {
231        public int numberOfImages;
232        public int[] delays;
233        public Image[] images;
234        public int length;
235        public boolean delayBugFound;
236        
237        @Override
238        public String toString() {
239            return "numberOfImages: " + numberOfImages + ", length: " + length + ", delayBugFound: " + delayBugFound + ", delays: " + Arrays.toString(delays);
240        }
241    }
242    
243    // Code copied from https://stackoverflow.com/questions/26801433/fix-frame-rate-of-animated-gif-in-java
244    // and modified
245    public static AnimationData getAnimationData(String fileName) {
246        try {
247            ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
248            File gifFile = new File(fileName);
249            ImageInputStream in = ImageIO.createImageInputStream(gifFile);
250            if (in == null) {
251                if (!FileUtil.exists(fileName)) {
252                    throw new RuntimeException("The file \"" + fileName + "\" doesn't exist");
253                }
254                throw new RuntimeException("in == null for file \"" + fileName + "\"");
255            }
256            reader.setInput(in);
257
258            int numberOfImages = reader.getNumImages(true);
259            
260            int minIndex = reader.getMinIndex();
261            if (minIndex != 0) {
262                System.out.println("minIndex: " + minIndex);
263            }
264            
265            IIOMetadata imageMetaData = reader.getImageMetadata(0);
266            String metaFormatName = imageMetaData.getNativeMetadataFormatName();
267            
268            boolean foundBug = false;
269            int length = 0;
270            int[] delays = new int[numberOfImages];
271            Image[] images = new Image[numberOfImages];
272            for (int i = 0; i < numberOfImages; i++) {
273                // Get metadata
274                IIOMetadataNode root = (IIOMetadataNode)reader.getImageMetadata(i).getAsTree(metaFormatName);
275
276                try {
277                    BufferedImage image = reader.read(i);
278                    images[i] = image;
279                } catch (IIOException e) {
280                    Util.printMessageWithSourceLine("IIOException" + (e.getMessage() != null ? ": " + e.getMessage() : ""));
281                } catch (ArrayIndexOutOfBoundsException e) {
282                    e.getStackTrace(); // Hack to make e.getMessage() work. I have no idea why.
283                    Util.printMessageWithSourceLine("ArrayIndexOutOfBoundsException" + (e.getMessage() != null ? ": " + e.getMessage() : ""));
284                }
285                
286                // Find GraphicControlExtension node
287                int nNodes = root.getLength();
288                for (int j = 0; j < nNodes; j++) {
289                    Node node = root.item(j);
290                    if (node.getNodeName().equalsIgnoreCase("GraphicControlExtension")) {
291                        // Get delay value
292                        String delayString = ((IIOMetadataNode)node).getAttribute("delayTime");
293    
294                        int delay = StringUtil.stringToInt(delayString) * 10;
295                        
296                        // Check if delay is bugged
297                        if (delay <= 0) {
298                            if (!foundBug) {
299                                foundBug = true;
300                                Util.printMessageWithSourceLine("foundBug: Delay: " + delay + "  " + fileName);
301                            }
302                            delay = DEFAULT_DELAY;
303                        }
304
305                        delays[i] = delay;
306                        length += delay;
307    
308                        break;
309                    }
310                }
311            }
312            
313            AnimationData animationData = new AnimationData();
314            animationData.numberOfImages = numberOfImages;
315            animationData.delayBugFound = foundBug;
316            animationData.length = length;
317            animationData.delays = delays;
318            animationData.images = images;
319            
320            return animationData;
321        } catch (IOException e) {
322            throw new RuntimeException(e);
323        }
324    }
325
326    public static void saveToFile(BufferedImage bufferedImage, String fileName) {
327        saveToFile(bufferedImage, fileName, "gif");
328    }
329    
330    public static void saveToFile(BufferedImage bufferedImage, String fileName, String format) {
331        File outputFile = new File(fileName);
332        try {
333            ImageIO.write(bufferedImage, format, outputFile);
334        } catch (IOException e) {
335            throw new RuntimeException(e);
336        }
337    }
338    
339    public static byte[] imageToByte(BufferedImage bufferedImage) {
340        return imageToByte(bufferedImage, "gif");
341    }
342    
343    public static byte[] imageToByte(BufferedImage bufferedImage, String format) {
344        try {
345            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
346            ImageIO.write(bufferedImage, format, byteArrayOutputStream);
347            return byteArrayOutputStream.toByteArray();
348        } catch (IOException e) {
349            throw new RuntimeException(e);
350        }
351    }
352
353    public static BufferedImage imageFromBytes(byte[] imageData) {
354        try {
355            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(imageData);
356            BufferedImage bufferedImage = ImageIO.read(byteArrayInputStream);
357            return bufferedImage;
358        } catch (IOException e) {
359            throw new RuntimeException(e);
360        }
361    }
362    
363    public static BufferedImage loadFromFile(String fileName) {
364        File inputFile = new File(fileName);
365        try {
366            BufferedImage bufferedImage = ImageIO.read(inputFile);
367            return bufferedImage;
368        } catch (IOException e) {
369            throw new RuntimeException(e);
370        }
371    }
372    
373    public static void drawSquare(int[] array, int arrayWidth, int x, int y, int w, int h, int color) {
374        for (int tx = x; tx < x + w; tx++) {
375            array[y * arrayWidth + tx] = color;
376            array[(y + h) * arrayWidth + tx] = color;
377        }
378
379        for (int ty = y; ty < y + h; ty++) {
380            array[ty * arrayWidth + x] = color;
381            array[ty * arrayWidth + x + w - 1] = color;
382        }
383    }
384
385    public static int[] getSubImage(int[] array, int arrayWidth, int x, int y, int w, int h) {
386        int[] result = new int[w * h];
387        int i = 0;
388        for (int ty = y; ty < y + h; ty++) {
389            for (int tx = x; tx < x + w; tx++) {
390                result[i++] = array[ty * arrayWidth + tx];
391            }
392        }
393        return result;
394    }
395
396    public static Cursor loadCursor(String fileName) {
397        return loadCursor(fileName, new Point(0, 0), FileUtil.calculateFileNameNoExtension(fileName));
398    }
399    
400    public static Cursor loadCursor(String fileName, Point hotSpot, String name) {
401        // Code copied from http://www.coderanch.com/t/330376/Swing-AWT-SWT-JFace/java/v-use-cursors-are-present
402        try {
403            // Load the toolkit.  The toolkit helps you with a lot
404            // of stuff... the toolkit is your friend...   [img]http://www.javaranch.com/ubb/wink.gif[/img]
405            Toolkit toolKit = Toolkit.getDefaultToolkit();
406            // Load the image.
407            Image cursorImage = toolKit.createImage(fileName);
408            // Create the cursor.  First arg is the image,
409            // second is the "hotspot", third is it's name.
410            // (Strangely not related to the name parameter
411            // of the getSystemCustomCursor() method... weird.)
412            return toolKit.createCustomCursor(cursorImage, hotSpot, name);
413        } catch (IndexOutOfBoundsException e) {
414            throw new RuntimeException(e);
415        }
416    }
417    
418    public static BufferedImage componentToImage(Component component) {
419        Rectangle bounds = component.getBounds();
420        return componentToImage(component, bounds);
421    }
422    
423    public static BufferedImage componentToImage(Component component, Rectangle rectangle) {
424        BufferedImage image = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
425        Graphics g = image.getGraphics();
426        g.setColor(component.getForeground());
427        g.setFont(component.getFont());
428        component.paintAll(g);
429        if (rectangle == null) {
430            rectangle = new Rectangle(0, 0, component.getWidth(), component.getHeight());
431        }
432        return image.getSubimage(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
433    }
434}