1 package org.robolectric.shadows; 2 3 import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; 4 import static java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; 5 import static java.awt.image.BufferedImage.TYPE_INT_ARGB; 6 import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE; 7 import static java.awt.image.BufferedImage.TYPE_INT_RGB; 8 import static javax.imageio.ImageIO.createImageInputStream; 9 10 import android.graphics.Bitmap; 11 import android.graphics.Bitmap.CompressFormat; 12 import android.graphics.Point; 13 import com.google.auto.value.AutoValue; 14 import java.awt.Graphics2D; 15 import java.awt.RenderingHints; 16 import java.awt.image.BufferedImage; 17 import java.io.IOException; 18 import java.io.InputStream; 19 import java.io.OutputStream; 20 import java.util.Iterator; 21 import java.util.Locale; 22 import javax.imageio.IIOException; 23 import javax.imageio.IIOImage; 24 import javax.imageio.ImageIO; 25 import javax.imageio.ImageReader; 26 import javax.imageio.ImageWriteParam; 27 import javax.imageio.ImageWriter; 28 import javax.imageio.stream.ImageInputStream; 29 import javax.imageio.stream.ImageOutputStream; 30 import org.robolectric.shadow.api.Shadow; 31 32 public class ImageUtil { 33 private static final String FORMAT_NAME_JPEG = "jpg"; 34 private static final String FORMAT_NAME_PNG = "png"; 35 private static boolean initialized; 36 37 /** Image information descriptor. */ 38 public static class ImageInfo { 39 40 public final int width; 41 public final int height; 42 public final String mimeType; 43 ImageInfo(int width, int height, String mimeType)44 ImageInfo(int width, int height, String mimeType) { 45 this.width = width; 46 this.height = height; 47 this.mimeType = mimeType; 48 } 49 } 50 getImageSizeFromStream(InputStream is)51 static Point getImageSizeFromStream(InputStream is) { 52 ImageInfo info = getImageInfoFromStream(is); 53 if (info == null) { 54 return null; 55 } else { 56 return new Point(info.width, info.height); 57 } 58 } 59 getImageInfoFromStream(InputStream is)60 static ImageInfo getImageInfoFromStream(InputStream is) { 61 if (!initialized) { 62 // Stops ImageIO from creating temp files when reading images 63 // from input stream. 64 ImageIO.setUseCache(false); 65 initialized = true; 66 } 67 68 try { 69 ImageInputStream imageStream = createImageInputStream(is); 70 Iterator<ImageReader> readers = ImageIO.getImageReaders(imageStream); 71 if (!readers.hasNext()) return null; 72 73 ImageReader reader = readers.next(); 74 try { 75 reader.setInput(imageStream); 76 return new ImageInfo( 77 reader.getWidth(0), 78 reader.getHeight(0), 79 "image/" + reader.getFormatName().toLowerCase(Locale.US)); 80 } finally { 81 reader.dispose(); 82 } 83 } catch (IOException e) { 84 throw new RuntimeException(e); 85 } 86 } 87 getImageFromStream(InputStream is)88 static RobolectricBufferedImage getImageFromStream(InputStream is) { 89 return getImageFromStream(null, is); 90 } 91 getImageFromStream(String fileName, InputStream is)92 static RobolectricBufferedImage getImageFromStream(String fileName, InputStream is) { 93 if (!initialized) { 94 // Stops ImageIO from creating temp files when reading images 95 // from input stream. 96 ImageIO.setUseCache(false); 97 initialized = true; 98 } 99 100 String format = null; 101 try { 102 ImageInputStream imageStream = createImageInputStream(is); 103 Iterator<ImageReader> readers = ImageIO.getImageReaders(imageStream); 104 if (!readers.hasNext()) { 105 return null; 106 } 107 108 ImageReader reader = readers.next(); 109 try { 110 reader.setInput(imageStream); 111 format = reader.getFormatName(); 112 int minIndex = reader.getMinIndex(); 113 BufferedImage image = reader.read(minIndex); 114 return RobolectricBufferedImage.create(image, ("image/" + format).toLowerCase(Locale.US)); 115 } finally { 116 reader.dispose(); 117 } 118 } catch (IOException e) { 119 Throwable cause = e.getCause(); 120 if (FORMAT_NAME_PNG.equalsIgnoreCase(format) 121 && cause instanceof IIOException 122 && cause.getMessage() != null 123 && cause.getMessage().contains("Invalid chunk length")) { 124 String pngFileName = "(" + (fileName == null ? "not given PNG file name" : fileName) + ")"; 125 System.err.println( 126 "The PNG file" 127 + pngFileName 128 + " cannot be decoded. This may be due to an OpenJDK issue with certain PNG files." 129 + " See https://github.com/robolectric/robolectric/issues/6812 for more details."); 130 } 131 throw new RuntimeException(e); 132 } 133 } 134 scaledBitmap(Bitmap src, Bitmap dst, boolean filter)135 static boolean scaledBitmap(Bitmap src, Bitmap dst, boolean filter) { 136 if (src == null || dst == null) { 137 return false; 138 } 139 int srcWidth = src.getWidth(); 140 int srcHeight = src.getHeight(); 141 int dstWidth = dst.getWidth(); 142 int dstHeight = dst.getHeight(); 143 if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { 144 return false; 145 } 146 BufferedImage before = ((ShadowLegacyBitmap) Shadow.extract(src)).getBufferedImage(); 147 if (before == null || before.getColorModel() == null) { 148 return false; 149 } 150 int imageType = getBufferedImageType(src.getConfig(), before.getColorModel().hasAlpha()); 151 BufferedImage after = new BufferedImage(dstWidth, dstHeight, imageType); 152 Graphics2D graphics2D = after.createGraphics(); 153 graphics2D.setRenderingHint( 154 RenderingHints.KEY_INTERPOLATION, 155 filter ? VALUE_INTERPOLATION_BILINEAR : VALUE_INTERPOLATION_NEAREST_NEIGHBOR); 156 graphics2D.drawImage(before, 0, 0, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, null); 157 graphics2D.dispose(); 158 ((ShadowLegacyBitmap) Shadow.extract(dst)).setBufferedImage(after); 159 return true; 160 } 161 writeToStream( Bitmap realBitmap, CompressFormat format, int quality, OutputStream stream)162 public static boolean writeToStream( 163 Bitmap realBitmap, CompressFormat format, int quality, OutputStream stream) { 164 if ((quality < 0) || (quality > 100)) { 165 throw new IllegalArgumentException("Quality out of bounds!"); 166 } 167 168 try { 169 ImageWriter writer = null; 170 Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(getFormatName(format)); 171 if (iter.hasNext()) { 172 writer = iter.next(); 173 } 174 if (writer == null) { 175 return false; 176 } 177 try (ImageOutputStream ios = ImageIO.createImageOutputStream(stream)) { 178 writer.setOutput(ios); 179 ImageWriteParam iwparam = writer.getDefaultWriteParam(); 180 iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 181 iwparam.setCompressionQuality((quality / 100f)); 182 int width = realBitmap.getWidth(); 183 int height = realBitmap.getHeight(); 184 boolean needAlphaChannel = needAlphaChannel(format); 185 BufferedImage bufferedImage = 186 ((ShadowLegacyBitmap) Shadow.extract(realBitmap)).getBufferedImage(); 187 if (bufferedImage == null) { 188 bufferedImage = 189 new BufferedImage( 190 realBitmap.getWidth(), realBitmap.getHeight(), BufferedImage.TYPE_INT_ARGB); 191 } 192 int outputImageType = getBufferedImageType(realBitmap.getConfig(), needAlphaChannel); 193 if (outputImageType != BufferedImage.TYPE_INT_ARGB) { 194 // re-encode image data with a type that is compatible with the output format. 195 BufferedImage outputBufferedImage = new BufferedImage(width, height, outputImageType); 196 Graphics2D g = outputBufferedImage.createGraphics(); 197 g.drawImage(bufferedImage, 0, 0, null); 198 g.dispose(); 199 bufferedImage = outputBufferedImage; 200 } 201 writer.write(null, new IIOImage(bufferedImage, null, null), iwparam); 202 ios.flush(); 203 writer.dispose(); 204 } 205 } catch (IOException ignore) { 206 return false; 207 } 208 209 return true; 210 } 211 getFormatName(CompressFormat compressFormat)212 private static String getFormatName(CompressFormat compressFormat) { 213 switch (compressFormat) { 214 case JPEG: 215 return FORMAT_NAME_JPEG; 216 case WEBP: 217 case WEBP_LOSSY: 218 case WEBP_LOSSLESS: 219 case PNG: 220 return FORMAT_NAME_PNG; 221 } 222 throw new UnsupportedOperationException("Cannot convert format: " + compressFormat); 223 } 224 needAlphaChannel(CompressFormat compressFormat)225 private static boolean needAlphaChannel(CompressFormat compressFormat) { 226 return !FORMAT_NAME_JPEG.equals(getFormatName(compressFormat)); 227 } 228 getBufferedImageType(Bitmap.Config config, boolean needAlphaChannel)229 private static int getBufferedImageType(Bitmap.Config config, boolean needAlphaChannel) { 230 if (config == null) { 231 return needAlphaChannel ? TYPE_INT_ARGB : TYPE_INT_RGB; 232 } 233 switch (config) { 234 case RGB_565: 235 return BufferedImage.TYPE_USHORT_565_RGB; 236 case RGBA_F16: 237 return needAlphaChannel ? TYPE_INT_ARGB_PRE : TYPE_INT_RGB; 238 case ALPHA_8: 239 case ARGB_4444: 240 case ARGB_8888: 241 case HARDWARE: 242 default: 243 return needAlphaChannel ? TYPE_INT_ARGB : TYPE_INT_RGB; 244 } 245 } 246 247 @AutoValue 248 abstract static class RobolectricBufferedImage { getBufferedImage()249 abstract BufferedImage getBufferedImage(); 250 getMimeType()251 abstract String getMimeType(); 252 getWidthAndHeight()253 public Point getWidthAndHeight() { 254 return new Point(getBufferedImage().getWidth(), getBufferedImage().getHeight()); 255 } 256 create(BufferedImage bufferedImage, String mimeType)257 static RobolectricBufferedImage create(BufferedImage bufferedImage, String mimeType) { 258 return new AutoValue_ImageUtil_RobolectricBufferedImage(bufferedImage, mimeType); 259 } 260 } 261 } 262