All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowBitmap Maven / Gradle / Ivy

package org.robolectric.shadows;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Parcel;
import android.util.DisplayMetrics;

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.ResName;
import org.robolectric.util.ReflectionHelpers;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;

import static org.robolectric.Shadows.shadowOf;

/**
 * Shadow for {@link android.graphics.Bitmap}.
 */
@SuppressWarnings({"UnusedDeclaration"})
@Implements(Bitmap.class)
public class ShadowBitmap {
  /** Number of bytes used internally to represent each pixel (in the {@link #colors} array) */
  private static final int INTERNAL_BYTES_PER_PIXEL = 4;

  @RealObject
  private Bitmap realBitmap;

  int createdFromResId = -1;
  String createdFromPath;
  InputStream createdFromStream;
  FileDescriptor createdFromFileDescriptor;
  byte[] createdFromBytes;
  private Bitmap createdFromBitmap;
  private int createdFromX = -1;
  private int createdFromY = -1;
  private int createdFromWidth = -1;
  private int createdFromHeight = -1;
  private int[] createdFromColors;
  private Matrix createdFromMatrix;
  private boolean createdFromFilter;
  private boolean hasAlpha;

  private int width;
  private int height;
  private int density;
  private int[] colors;
  private Bitmap.Config config;
  private boolean mutable;
  private String description = "";
  private boolean recycled = false;
  private boolean hasMipMap;

  /**
   * Returns a textual representation of the appearance of the object.
   *
   * @param bitmap the bitmap to visualize
   * @return Textual representation of the appearance of the object.
   */
  public static String visualize(Bitmap bitmap) {
    return shadowOf(bitmap).getDescription();
  }

  /**
   * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
   * was not copied from another instance.
   *
   * @return Original Bitmap from which this Bitmap was created.
   */
  public Bitmap getCreatedFromBitmap() {
    return createdFromBitmap;
  }

  /**
   * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created
   * from a resource.
   *
   * @return Resource ID from which this Bitmap was created.
   */
  public int getCreatedFromResId() {
    return createdFromResId;
  }

  /**
   * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
   * path.
   *
   * @return Path from which this Bitmap was created.
   */
  public String getCreatedFromPath() {
    return createdFromPath;
  }

  /**
   * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
   * created from a stream.
   *
   * @return InputStream from which this Bitmap was created.
   */
  public InputStream getCreatedFromStream() {
    return createdFromStream;
  }

  /**
   * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
   * bytes.
   *
   * @return Bytes from which this Bitmap was created.
   */
  public byte[] getCreatedFromBytes() {
    return createdFromBytes;
  }

  /**
   * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   *
   * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
   */
  public int getCreatedFromX() {
    return createdFromX;
  }

  /**
   * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   *
   * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   */
  public int getCreatedFromY() {
    return createdFromY;
  }

  /**
   * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   *
   * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   */
  public int getCreatedFromWidth() {
    return createdFromWidth;
  }

  /**
   * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   */
  public int getCreatedFromHeight() {
    return createdFromHeight;
  }

  /**
   * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
   * from a color array.
   * @return Color array from which this Bitmap was created.
   */
  public int[] getCreatedFromColors() {
    return createdFromColors;
  }

  /**
   * Matrix from which this Bitmap's content was transformed, or {@code null}.
   * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
   */
  public Matrix getCreatedFromMatrix() {
    return createdFromMatrix;
  }

  /**
   * {@code true} if this Bitmap was created with filtering.
   * @return {@code true} if this Bitmap was created with filtering.
   */
  public boolean getCreatedFromFilter() {
    return createdFromFilter;
  }

  @Implementation
  public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
    try {
      stream.write((description + " compressed as " + format + " with quality " + quality).getBytes());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    return true;
  }

  @Implementation
  public static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
    return createBitmap((DisplayMetrics) null, width, height, config);
  }

  @Implementation
  public static Bitmap createBitmap(DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config, boolean hasAlpha) {
    return createBitmap((DisplayMetrics) null, width, height, config);
  }

  @Implementation
  public static Bitmap createBitmap(DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
    if (width <= 0 || height <= 0) {
      throw new IllegalArgumentException("width and height must be > 0");
    }
    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = shadowOf(scaledBitmap);
    shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");

    shadowBitmap.width = width;
    shadowBitmap.height = height;
    shadowBitmap.config = config;
    shadowBitmap.setMutable(true);
    if (displayMetrics != null) {
      shadowBitmap.density = displayMetrics.densityDpi;
    }
    shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, shadowBitmap.getWidth(), 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight());
    return scaledBitmap;
  }

  @Implementation
  public static Bitmap createBitmap(Bitmap src) {
    ShadowBitmap shadowBitmap = shadowOf(src);
    shadowBitmap.appendDescription(" created from Bitmap object");
    return src;
  }

  @Implementation
  public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) {
    if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
      return src; // Return the original.
    }

    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = shadowOf(scaledBitmap);

    shadowBitmap.appendDescription(shadowOf(src).getDescription());
    shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
    if (filter) {
      shadowBitmap.appendDescription(" with filter " + filter);
    }

    shadowBitmap.createdFromBitmap = src;
    shadowBitmap.createdFromFilter = filter;
    shadowBitmap.width = dstWidth;
    shadowBitmap.height = dstHeight;
    shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, 0, 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight());
    return scaledBitmap;
  }

  @Implementation
  public static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height) {
    if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight()) {
      return src; // Return the original.
    }

    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = shadowOf(newBitmap);

    shadowBitmap.appendDescription(shadowOf(src).getDescription());
    shadowBitmap.appendDescription(" at (" + x + "," + y);
    shadowBitmap.appendDescription(" with width " + width + " and height " + height);

    shadowBitmap.createdFromBitmap = src;
    shadowBitmap.createdFromX = x;
    shadowBitmap.createdFromY = y;
    shadowBitmap.createdFromWidth = width;
    shadowBitmap.createdFromHeight = height;
    shadowBitmap.width = width;
    shadowBitmap.height = height;
    return newBitmap;
  }

  @Implementation
  public void setPixels(int[] pixels, int offset, int stride,
                        int x, int y, int width, int height) {
    this.colors = pixels;
  }

  @Implementation
  public static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
    if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight() && (matrix == null || matrix.isIdentity())) {
      return src; // Return the original.
    }

    if (x + width > src.getWidth()) {
      throw new IllegalArgumentException("x + width must be <= bitmap.width()");
    }
    if (y + height > src.getHeight()) {
      throw new IllegalArgumentException("y + height must be <= bitmap.height()");
    }

    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = shadowOf(newBitmap);

    shadowBitmap.appendDescription(shadowOf(src).getDescription());
    shadowBitmap.appendDescription(" at (" + x + "," + y);
    shadowBitmap.appendDescription(" with width " + width + " and height " + height);
    if (matrix != null) {
      shadowBitmap.appendDescription(" using matrix " + matrix);
    }
    if (filter) {
      shadowBitmap.appendDescription(" with filter");
    }

    shadowBitmap.createdFromBitmap = src;
    shadowBitmap.createdFromX = x;
    shadowBitmap.createdFromY = y;
    shadowBitmap.createdFromWidth = width;
    shadowBitmap.createdFromHeight = height;
    shadowBitmap.createdFromMatrix = matrix;
    shadowBitmap.createdFromFilter = filter;
    shadowBitmap.width = width;
    shadowBitmap.height = height;
    return newBitmap;
  }

  @Implementation
  public static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config) {
    if (colors.length != width * height) {
      throw new IllegalArgumentException("array length (" + colors.length + ") did not match width * height (" + (width * height) + ")");
    }

    Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
    ShadowBitmap shadowBitmap = shadowOf(newBitmap);

    shadowBitmap.setMutable(false);
    shadowBitmap.createdFromColors = colors;
    shadowBitmap.colors = new int[colors.length];
    System.arraycopy(colors, 0, shadowBitmap.colors, 0, colors.length);
    return newBitmap;
  }

  @Implementation
  public int getPixel(int x, int y) {
    internalCheckPixelAccess(x, y);
    if (colors != null) {
      // Note that getPixel() returns a non-premultiplied ARGB value; if
      // config is RGB_565, our return value will likely be more precise than
      // on a physical device, since it needs to map each color component from
      // 5 or 6 bits to 8 bits.
      return colors[y * getWidth() + x];
    } else {
      return 0;
    }
  }

  @Implementation
  public void setPixel(int x, int y, int color) {
    if (isRecycled()) {
      throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
    } else if (!isMutable()) {
      throw new IllegalStateException("Bitmap is immutable");
    }
    internalCheckPixelAccess(x, y);
    if (colors == null) {
      colors = new int[getWidth() * getHeight()];
    }
    colors[y * getWidth() + x] = color;
  }

  /**
   * Note that this method will return a RuntimeException unless:
   * - {@code pixels} has the same length as the number of pixels of the bitmap.
   * - {@code x = 0}
   * - {@code y = 0}
   * - {@code width} and {@code height} height match the current bitmap's dimensions.
   */
  @Implementation
  public void getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height) {
    if (x != 0 ||
        y != 0 ||
        width != getWidth() ||
        height != getHeight() ||
        pixels.length != colors.length) {
      throw new RuntimeException("Not implemented.");
    }
    System.arraycopy(colors, 0, pixels, 0, colors.length);
  }

  @Implementation
  public int getRowBytes() {
    return getBytesPerPixel(config) * getWidth();
  }

  @Implementation
  public int getByteCount() {
    return getRowBytes() * getHeight();
  }

  @Implementation
  public void recycle() {
    recycled = true;
  }

  @Implementation
  public final boolean isRecycled() {
    return recycled;
  }

  @Implementation
  public Bitmap copy(Bitmap.Config config, boolean isMutable) {
    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = shadowOf(newBitmap);
    shadowBitmap.createdFromBitmap = realBitmap;
    shadowBitmap.config = config;
    shadowBitmap.mutable = isMutable;
    return newBitmap;
  }

  @Implementation
  public final Bitmap.Config getConfig() {
    return config;
  }

  @Implementation
  public void setConfig(Bitmap.Config config) {
    this.config = config;
  }

  @Implementation
  public final boolean isMutable() {
    return mutable;
  }

  public void setMutable(boolean mutable) {
    this.mutable = mutable;
  }

  public void appendDescription(String s) {
    description += s;
  }

  public void setDescription(String s) {
    description = s;
  }

  public String getDescription() {
    return description;
  }

  @Implementation
  public final boolean hasAlpha() {
    return hasAlpha;
  }

  @Implementation
  public void setHasAlpha(boolean hasAlpha) {
    this.hasAlpha = hasAlpha;
  }

  @Implementation
  public final boolean hasMipMap() {
    return hasMipMap;
  }

  @Implementation
  public final void setHasMipMap(boolean hasMipMap) {
    this.hasMipMap = hasMipMap;
  }

  @Implementation
  public void setWidth(int width) {
    this.width = width;
  }

  @Implementation
  public int getWidth() {
    return width;
  }

  @Implementation
  public void setHeight(int height) {
    this.height = height;
  }

  @Implementation
  public int getHeight() {
    return height;
  }

  @Implementation
  public void setDensity(int density) {
    this.density = density;
  }

  @Implementation
  public int getDensity() {
    return density;
  }

  @Implementation
  public int getGenerationId() {
    return 0;
  }

  @Implementation
  public Bitmap createAshmemBitmap() {
    return realBitmap;
  }

  @Implementation
  public void eraseColor(int c) {

  }

  @Implementation
  public void writeToParcel(Parcel p, int flags) {
    p.writeInt(width);
    p.writeInt(height);
    p.writeSerializable(config);
    p.writeIntArray(colors);
  }

  @Implementation
  public static Bitmap nativeCreateFromParcel(Parcel p) {
    int parceledWidth = p.readInt();
    int parceledHeight = p.readInt();
    Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();

    int[] parceledColors = new int[parceledHeight * parceledWidth];
    p.readIntArray(parceledColors);

    return createBitmap(parceledColors, parceledWidth, parceledHeight, parceledConfig);
  }

  @Implementation
  public void copyPixelsFromBuffer(Buffer dst) {
    if (isRecycled()) {
      throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
    }

    // See the related comment in #copyPixelsToBuffer(Buffer).
    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL
              + " bytes per pixel are supported");
    }
    if (!(dst instanceof ByteBuffer)) {
      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
    }

    ByteBuffer byteBuffer = (ByteBuffer) dst;
    if (byteBuffer.remaining() < colors.length * INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Buffer not large enough for pixels");
    }

    for (int i = 0; i < colors.length; i++) {
      colors[i] = byteBuffer.getInt();
    }
  }

  @Implementation
  public void copyPixelsToBuffer(Buffer dst) {
    // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
    // internally. Clients of this API probably expect that the buffer size must be >=
    // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
    // configs that value would be smaller then the buffer size we actually need.
    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL
              + " bytes per pixel are supported");
    }

    if (!(dst instanceof ByteBuffer)) {
      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
    }

    ByteBuffer byteBuffer = (ByteBuffer) dst;
    for (int color : colors) {
      byteBuffer.putInt(color);
    }
  }

  @Override
  public String toString() {
    return "Bitmap{description='" + description + '\'' + ", width=" + width + ", height=" + height + '}';
  }

  public Bitmap getRealBitmap() {
    return realBitmap;
  }

  public static int getBytesPerPixel(Bitmap.Config config) {
    if (config == null) {
      throw new NullPointerException("Bitmap config was null.");
    }
    switch (config) {
      case ARGB_8888:
        return 4;
      case RGB_565:
      case ARGB_4444:
        return 2;
      case ALPHA_8:
        return 1;
      default:
        throw new IllegalArgumentException("Unknown bitmap config: " + config);
    }
  }

  public void setCreatedFromResId(int resId, ResName resName) {
    this.createdFromResId = resId;
    appendDescription(" for resource:" + resName.getFullyQualifiedName());
  }

  private void internalCheckPixelAccess(int x, int y) {
    if (x < 0) {
      throw new IllegalArgumentException("x must be >= 0");
    }
    if (y < 0) {
      throw new IllegalArgumentException("y must be >= 0");
    }
    if (x >= getWidth()) {
      throw new IllegalArgumentException("x must be < bitmap.width()");
    }
    if (y >= getHeight()) {
      throw new IllegalArgumentException("y must be < bitmap.height()");
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy