xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
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