/* * $Id$ * * Copyright (c) 2007-2010 by Joel Uckelman * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools.image; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.PixelGrabber; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import javax.swing.ImageIcon; import VASSAL.Info; import VASSAL.tools.ErrorDialog; import VASSAL.tools.io.TemporaryFileFactory; public class ImageUtils { private ImageUtils() {} // FIXME: We should fix this, eventually. // negative, because historically we've done it this way private static final double DEGTORAD = -Math.PI/180.0; private static final GeneralFilter.Filter upscale = new GeneralFilter.MitchellFilter(); private static final GeneralFilter.Filter downscale = new GeneralFilter.Lanczos3Filter(); @Deprecated public static final String SCALER_ALGORITHM = "scalerAlgorithm"; //$NON-NLS-1$ private static final Map<RenderingHints.Key,Object> defaultHints = new HashMap<RenderingHints.Key,Object>(); static { // Initialise Image prefs prior to Preferences being read. // set up map for creating default RenderingHints defaultHints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); defaultHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } /** @deprecated All scaling is done with the high-quality scaler now. */ @Deprecated public static void setHighQualityScaling(boolean b) {} public static RenderingHints getDefaultHints() { return new RenderingHints(defaultHints); } public static Rectangle transform(Rectangle srect, double scale, double angle) { final AffineTransform t = AffineTransform.getRotateInstance( DEGTORAD*angle, srect.getCenterX(), srect.getCenterY()); t.scale(scale, scale); return t.createTransformedShape(srect).getBounds(); } public static BufferedImage transform(BufferedImage src, double scale, double angle) { return transform(src, scale, angle, getDefaultHints()); } public static BufferedImage transform(BufferedImage src, double scale, double angle, RenderingHints hints) { // bail on null source if (src == null) return null; // nothing to do, return source if (scale == 1.0 && angle == 0.0) { return src; } // return null image if scaling makes source vanish if (src.getWidth() * scale == 0 || src.getHeight() * scale == 0) { return NULL_IMAGE; } // use the default hints if we weren't given any if (hints == null) hints = getDefaultHints(); if (scale == 1.0 && angle % 90.0 == 0.0) { // this is an unscaled quadrant rotation, we can do this simply hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); final Rectangle ubox = getBounds(src); final Rectangle tbox = transform(ubox, scale, angle); // keep opaque destination for orthogonal rotation of an opaque source final BufferedImage trans = createCompatibleImage( tbox.width, tbox.height, src.getTransparency() != BufferedImage.OPAQUE ); final AffineTransform t = new AffineTransform(); t.translate(-tbox.x, -tbox.y); t.rotate(DEGTORAD*angle, ubox.getCenterX(), ubox.getCenterY()); t.scale(scale, scale); t.translate(ubox.x, ubox.y); final Graphics2D g = trans.createGraphics(); g.setRenderingHints(hints); g.drawImage(src, t, null); g.dispose(); return trans; } else { if (angle != 0.0) { final Rectangle ubox = getBounds(src); // FIXME: this duplicates the standard scaling case // FIXME: check whether AffineTransformOp is faster final Rectangle rbox = transform(ubox, 1.0, angle); // keep opaque destination for orthogonal rotation of an opaque source final BufferedImage rot = createCompatibleImage( rbox.width, rbox.height, src.getTransparency() != BufferedImage.OPAQUE || angle % 90.0 != 0.0 ); // FIXME: rotation via bilinear interpolation probably decreases quality final AffineTransform tx = new AffineTransform(); tx.translate(-rbox.x, -rbox.y); tx.rotate(DEGTORAD*angle, ubox.getCenterX(), ubox.getCenterY()); tx.translate(ubox.x, ubox.y); final Graphics2D g = rot.createGraphics(); g.setRenderingHints(hints); g.drawImage(src, tx, null); g.dispose(); src = rot; } if (scale != 1.0) { src = coerceToIntType(src); final Rectangle sbox = transform(getBounds(src), scale, 0.0); // return null image if scaling makes source vanish if (sbox.width == 0 || sbox.height == 0) { return NULL_IMAGE; } final BufferedImage dst = GeneralFilter.zoom(sbox, src, scale > 1.0 ? upscale : downscale); return toCompatibleImage(dst); } else { return src; } } } @Deprecated public static BufferedImage transform(BufferedImage src, double scale, double angle, RenderingHints hints, int quality) { return transform(src, scale, angle, hints); } @SuppressWarnings("fallthrough") public static BufferedImage coerceToIntType(BufferedImage img) { // ensure that img is a type which GeneralFilter can handle switch (img.getType()) { case BufferedImage.TYPE_INT_RGB: case BufferedImage.TYPE_INT_ARGB: case BufferedImage.TYPE_INT_ARGB_PRE: case BufferedImage.TYPE_INT_BGR: return img; default: return toType(img, img.getTransparency() == BufferedImage.OPAQUE ? BufferedImage.TYPE_INT_RGB : getCompatibleTranslucentImageType() == BufferedImage.TYPE_INT_ARGB ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_ARGB_PRE); } } /** * @param im * @return the boundaries of this image, where (0,0) is the * pseudo-center of the image */ public static Rectangle getBounds(BufferedImage im) { return new Rectangle(-im.getWidth()/2, -im.getHeight()/2, im.getWidth(), im.getHeight()); } public static Rectangle getBounds(Dimension d) { return new Rectangle(-d.width / 2, -d.height / 2, d.width, d.height); } /** @deprecated Use {@link #getImageSize(String,InputStream)} instead. */ @Deprecated public static Dimension getImageSize(InputStream in) throws IOException { return getImageSize("", in); } private static final TemporaryFileFactory tfac = new TemporaryFileFactory() { public File create() throws IOException { return File.createTempFile("img", null, Info.getTempDir()); } }; private static final ImageLoader loader = new ImageIOImageLoader(new FallbackImageTypeConverter(tfac)); public static Dimension getImageSize(String name, InputStream in) throws ImageIOException { return loader.size(name, in); } /** @deprecated Use {@link #getImage(String,InputStream)} instead. */ @Deprecated public static BufferedImage getImage(InputStream in) throws IOException { return getImage("", in); } public static BufferedImage getImageResource(String name) throws ImageIOException { final InputStream in = ImageUtils.class.getResourceAsStream(name); if (in == null) throw new ImageNotFoundException(name); return getImage(name, in); } public static BufferedImage getImage(String name, InputStream in) throws ImageIOException { return loader.load( name, in, compatOpaqueImageType, compatTranslImageType, true ); } public static BufferedImage toType(BufferedImage src, int type) { final BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), type); final Graphics2D g = dst.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); return dst; } public static Image forceLoad(Image img) { // ensure that the image is loaded return new ImageIcon(img).getImage(); } public static boolean isTransparent(Image img) { // determine whether this image has an alpha channel final PixelGrabber pg = new PixelGrabber(img, 0, 0, 1, 1, false); try { pg.grabPixels(); } catch (InterruptedException e) { ErrorDialog.bug(e); } return pg.getColorModel().hasAlpha(); } public static boolean isTransparent(BufferedImage img) { return img.getTransparency() != BufferedImage.OPAQUE; } /** * Transform an <code>Image</code> to a <code>BufferedImage</code>. * * @param src the <code>Image</code> to transform */ public static BufferedImage toBufferedImage(Image src) { if (src == null) return null; if (src instanceof BufferedImage) return toCompatibleImage((BufferedImage) src); // ensure that the image is loaded src = forceLoad(src); final BufferedImage dst = createCompatibleImage( src.getWidth(null), src.getHeight(null), isTransparent(src) ); final Graphics2D g = dst.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); return dst; } protected static final boolean IS_MAC_RETINA; static { final Object o = Toolkit.getDefaultToolkit().getDesktopProperty( "apple.awt.contentScaleFactor" ); IS_MAC_RETINA = (o instanceof Number) && ((Number) o).doubleValue() == 2.0; } public static boolean isMacRetina() { return IS_MAC_RETINA; } private static boolean isHeadless() { return GraphicsEnvironment.isHeadless(); } private static GraphicsConfiguration getGraphicsConfiguration() { return GraphicsEnvironment.getLocalGraphicsEnvironment(). getDefaultScreenDevice().getDefaultConfiguration(); } protected static final BufferedImage compatOpaqueImage; protected static final BufferedImage compatTransImage; protected static final int compatOpaqueImageType; protected static final int compatTranslImageType; static { BufferedImage oimg; BufferedImage timg; if (isHeadless()) { oimg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); timg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); } else { final GraphicsConfiguration gc = getGraphicsConfiguration(); oimg = gc.createCompatibleImage(1,1, BufferedImage.OPAQUE); timg = gc.createCompatibleImage(1,1, BufferedImage.TRANSLUCENT); // Bug workaround: MacOX X machines with Retina displays are incapable // of painting TYPE_INT_ARGB_PRE images, despite that these systems // return that type as the "compatible" image type. if (isMacRetina()) { if (oimg.getType() == BufferedImage.TYPE_INT_ARGB_PRE) { oimg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); } if (timg.getType() == BufferedImage.TYPE_INT_ARGB_PRE) { timg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); } } } compatOpaqueImage = oimg; compatTransImage = timg; compatOpaqueImageType = compatOpaqueImage.getType(); compatTranslImageType = compatTransImage.getType(); } public static final BufferedImage NULL_IMAGE = createCompatibleImage(1,1); public static int getCompatibleImageType() { return compatOpaqueImageType; } public static int getCompatibleTranslucentImageType() { return compatTranslImageType; } public static int getCompatibleImageType(boolean transparent) { return transparent ? compatTranslImageType : compatOpaqueImageType; } public static int getCompatibleImageType(BufferedImage img) { return getCompatibleImageType(isTransparent(img)); } public static BufferedImage createCompatibleImage(int w, int h) { final ColorModel cm = compatOpaqueImage.getColorModel(); final WritableRaster wr = cm.createCompatibleWritableRaster(w, h); return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null); } public static BufferedImage createCompatibleImage(int w, int h, boolean transparent) { return transparent ? createCompatibleTranslucentImage(w, h) : createCompatibleImage(w, h); } public static BufferedImage createCompatibleTranslucentImage(int w, int h) { final ColorModel cm = compatTransImage.getColorModel(); final WritableRaster wr = cm.createCompatibleWritableRaster(w, h); return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null); } public static BufferedImage toCompatibleImage(BufferedImage src) { if ((src.getColorModel().equals(compatOpaqueImage.getColorModel()) && src.getTransparency() == compatOpaqueImage.getTransparency()) || (src.getColorModel().equals(compatTransImage.getColorModel()) && src.getTransparency() == compatTransImage.getTransparency())) { return src; } final BufferedImage dst = createCompatibleImage( src.getWidth(), src.getHeight(), isTransparent(src) ); final Graphics2D g = dst.createGraphics(); g.drawImage(src, 0, 0, null); g.dispose(); return dst; } public static boolean isCompatibleImage(BufferedImage img) { return img.getType() == getCompatibleImageType(img.getTransparency() != BufferedImage.OPAQUE); } /* * What Image suffixes does Vassal know about? * Used by the MassPieceLoader to identify candidate images. */ public static final String GIF_SUFFIX = ".gif"; public static final String PNG_SUFFIX = ".png"; public static final String SVG_SUFFIX = ".svg"; public static final String JPG_SUFFIX = ".jpg"; public static final String JPEG_SUFFIX = ".jpeg"; public static final String[] IMAGE_SUFFIXES = { GIF_SUFFIX, PNG_SUFFIX, SVG_SUFFIX, JPG_SUFFIX, JPEG_SUFFIX }; public static boolean hasImageSuffix(String name) { final String s = name.toLowerCase(); for (String suffix : IMAGE_SUFFIXES) { if (s.endsWith(suffix)) { return true; } } return false; } public static String stripImageSuffix(String name) { final String s = name.toLowerCase(); for (String suffix : IMAGE_SUFFIXES) { if (s.endsWith(suffix)) { return name.substring(0, name.length()-suffix.length()); } } return name; } }