Back to Repositories

Testing Bitmap Transformation Operations in bumptech/glide

This test suite validates the bitmap transformation utilities in Glide, focusing on image resizing, rotation, and EXIF orientation handling. It ensures proper functionality of critical image manipulation operations like fitCenter, centerCrop, and orientation adjustments.

Test Coverage Overview

The test suite provides comprehensive coverage of bitmap transformation operations including:
  • Fit center transformations for various image dimensions and aspect ratios
  • Center crop operations with alpha channel handling
  • EXIF orientation processing and rotation matrices
  • Edge cases like null configs and color space preservation

Implementation Analysis

The testing approach uses JUnit with Robolectric for Android bitmap operations. It employs extensive mocking via Mockito to isolate bitmap pool interactions and matrix transformations. The tests validate both functional correctness and pixel-level accuracy of transformations.

Key patterns include setup/teardown with @Before, systematic test organization by transformation type, and thorough assertion checking.

Technical Details

Testing tools and configuration:
  • JUnit 4 test framework
  • Robolectric for Android SDK simulation
  • Mockito for mocking and verification
  • BitmapPool interface for resource management
  • Custom assertion utilities for aspect ratio and dimension validation

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Comprehensive edge case coverage
  • Precise pixel-level verification
  • Efficient resource handling with bitmap pooling
  • Clear test method naming and organization
  • Thorough documentation of test scenarios

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/TransformationUtilsTest.java

            
package com.bumptech.glide.load.resource.bitmap;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ColorSpace;
import android.graphics.Matrix;
import android.os.Build.VERSION_CODES;
import androidx.exifinterface.media.ExifInterface;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.tests.Util;
import com.google.common.collect.Range;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
public class TransformationUtilsTest {

  @Mock private BitmapPool bitmapPool;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class)))
        .thenAnswer(new Util.CreateBitmap());
  }

  @Test
  public void testFitCenterWithWideBitmap() {
    final int maxSide = 500;

    Bitmap wide = Bitmap.createBitmap(2000, 100, Bitmap.Config.ARGB_8888);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, wide, maxSide, maxSide);

    assertHasOriginalAspectRatio(wide, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  @Test
  public void testFitCenterWithSmallWideBitmap() {
    final int maxSide = 500;

    Bitmap smallWide = Bitmap.createBitmap(400, 40, Bitmap.Config.ARGB_8888);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallWide, maxSide, maxSide);

    assertHasOriginalAspectRatio(smallWide, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  @Test
  public void testFitCenterWithTallBitmap() {
    final int maxSide = 500;

    Bitmap tall = Bitmap.createBitmap(65, 3000, Bitmap.Config.ARGB_8888);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, tall, maxSide, maxSide);

    assertHasOriginalAspectRatio(tall, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  @Test
  public void testFitCenterWithSmallTallBitmap() {
    final int maxSide = 500;

    Bitmap smallTall = Bitmap.createBitmap(10, 400, Bitmap.Config.ARGB_8888);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallTall, maxSide, maxSide);

    assertHasOriginalAspectRatio(smallTall, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  @Test
  public void testFitCenterWithSquareBitmap() {
    final int maxSide = 500;

    Bitmap square = Bitmap.createBitmap(600, 600, Bitmap.Config.ARGB_8888);
    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, square, maxSide, maxSide);

    assertHasOriginalAspectRatio(square, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  @Test
  public void testFitCenterWithTooSmallSquareBitmap() {
    final int maxSide = 500;

    Bitmap smallSquare = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallSquare, maxSide, maxSide);

    assertHasOriginalAspectRatio(smallSquare, transformed);
    assertBitmapFitsExactlyWithinBounds(maxSide, transformed);
  }

  // Test for Issue #195.
  @Test
  public void testFitCenterUsesFloorInsteadOfRoundingForOutputBitmapSize() {
    Bitmap toTransform = Bitmap.createBitmap(1230, 1640, Bitmap.Config.RGB_565);

    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toTransform, 1075, 1366);

    assertEquals(1024, transformed.getWidth());
    assertEquals(1366, transformed.getHeight());
  }

  @Test
  public void testFitCenterReturnsGivenBitmapIfGivenBitmapMatchesExactly() {
    Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444);
    Bitmap transformed =
        TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth(), toFit.getHeight());
    assertTrue(toFit == transformed);
  }

  @Test
  public void testFitCenterReturnsGivenBitmapIfGivenBitmapWidthMatchesExactly() {
    Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444);
    Bitmap transformed =
        TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth(), toFit.getHeight() * 2);
    assertTrue(toFit == transformed);
  }

  @Test
  public void testFitCenterReturnsGivenBitmapIfGivenBitmapHeightMatchesExactly() {
    Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444);
    Bitmap transformed =
        TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth() * 2, toFit.getHeight());
    assertTrue(toFit == transformed);
  }

  @Test
  public void testCenterCropReturnsGivenBitmapIfGivenBitmapExactlyMatchesGivenDimensions() {
    Bitmap toCrop = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888);
    Bitmap transformed =
        TransformationUtils.centerCrop(bitmapPool, toCrop, toCrop.getWidth(), toCrop.getHeight());

    // Robolectric incorrectly implements equals() for Bitmaps, we want the original object not
    // just an equivalent.
    assertTrue(toCrop == transformed);
  }

  @Test
  @Config(sdk = 19)
  public void testFitCenterHandlesBitmapsWithNullConfigs() {
    Bitmap toFit = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
    toFit.setConfig(null);
    Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toFit, 50, 50);
    assertEquals(Bitmap.Config.ARGB_8888, transformed.getConfig());
  }

  @Test
  public void testCenterCropSetsOutBitmapToHaveAlphaIfInBitmapHasAlphaAndOutBitmapIsReused() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
    reset(bitmapPool);
    when(bitmapPool.get(eq(50), eq(50), eq(Bitmap.Config.ARGB_8888))).thenReturn(toReuse);

    toReuse.setHasAlpha(false);
    toTransform.setHasAlpha(true);

    Bitmap result =
        TransformationUtils.centerCrop(
            bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight());

    assertEquals(toReuse, result);
    assertTrue(result.hasAlpha());
  }

  @Test
  public void
      testCenterCropSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlphaAndOutBitmapIsReused() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
    reset(bitmapPool);
    when(bitmapPool.get(eq(50), eq(50), eq(Bitmap.Config.ARGB_8888))).thenReturn(toReuse);

    toReuse.setHasAlpha(true);
    toTransform.setHasAlpha(false);

    Bitmap result =
        TransformationUtils.centerCrop(
            bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight());

    assertEquals(toReuse, result);
    assertFalse(result.hasAlpha());
  }

  @Test
  public void testCenterCropSetsOutBitmapToHaveAlphaIfInBitmapHasAlpha() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    toTransform.setHasAlpha(true);

    Bitmap result =
        TransformationUtils.centerCrop(
            bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2);

    assertTrue(result.hasAlpha());
  }

  @Test
  @Config(sdk = 19)
  public void testCenterCropHandlesBitmapsWithNullConfigs() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
    toTransform.setConfig(null);

    Bitmap transformed = TransformationUtils.centerCrop(bitmapPool, toTransform, 50, 50);

    assertEquals(Bitmap.Config.ARGB_8888, transformed.getConfig());
  }

  @Test
  public void testCenterCropSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlpha() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    toTransform.setHasAlpha(false);

    Bitmap result =
        TransformationUtils.centerCrop(
            bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2);

    assertFalse(result.hasAlpha());
  }

  @Test
  public void testFitCenterSetsOutBitmapToHaveAlphaIfInBitmapHasAlphaAndOutBitmapIsReused() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
    reset(bitmapPool);
    when(bitmapPool.get(eq(toReuse.getWidth()), eq(toReuse.getHeight()), eq(toReuse.getConfig())))
        .thenReturn(toReuse);

    toReuse.setHasAlpha(false);
    toTransform.setHasAlpha(true);

    Bitmap result =
        TransformationUtils.fitCenter(
            bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight());

    assertEquals(toReuse, result);
    assertTrue(result.hasAlpha());
  }

  @Test
  public void
      testFitCenterSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlphaAndOutBitmapIsReused() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
    reset(bitmapPool);
    when(bitmapPool.get(eq(toReuse.getWidth()), eq(toReuse.getHeight()), eq(toReuse.getConfig())))
        .thenReturn(toReuse);

    toReuse.setHasAlpha(true);
    toTransform.setHasAlpha(false);

    Bitmap result =
        TransformationUtils.fitCenter(
            bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight());

    assertEquals(toReuse, result);
    assertFalse(result.hasAlpha());
  }

  @Test
  public void testFitCenterSetsOutBitmapToHaveAlphaIfInBitmapHasAlpha() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    toTransform.setHasAlpha(true);

    Bitmap result =
        TransformationUtils.fitCenter(
            bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2);

    assertTrue(result.hasAlpha());
  }

  @Test
  public void testFitCenterSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlpha() {
    Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    toTransform.setHasAlpha(false);

    Bitmap result =
        TransformationUtils.fitCenter(
            bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2);

    assertFalse(result.hasAlpha());
  }

  private static void assertHasOriginalAspectRatio(Bitmap original, Bitmap transformed) {
    double originalAspectRatio = (double) original.getWidth() / (double) original.getHeight();
    double transformedAspectRatio =
        (double) transformed.getWidth() / (double) transformed.getHeight();

    assertThat(transformedAspectRatio)
        .isIn(Range.open(originalAspectRatio - 0.05f, originalAspectRatio + 0.05f));
  }

  private static void assertBitmapFitsExactlyWithinBounds(int maxSide, Bitmap bitmap) {
    final int width = bitmap.getWidth();
    final int height = bitmap.getHeight();

    assertThat(width).isIn(Range.atMost(maxSide));
    assertThat(height).isIn(Range.atMost(maxSide));

    assertTrue("one side must match maxSide", width == maxSide || height == maxSide);
  }

  @Test
  public void testGetExifOrientationDegrees() {
    assertEquals(
        0, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_NORMAL));
    assertEquals(
        90, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_TRANSPOSE));
    assertEquals(
        90, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_90));
    assertEquals(
        180, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_180));
    assertEquals(
        180,
        TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_FLIP_VERTICAL));
    assertEquals(
        270, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_TRANSVERSE));
    assertEquals(
        270, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_270));
  }

  @Test
  public void testRotateImage() {
    Bitmap toRotate = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888);
    toRotate.setPixel(0, 0, Color.BLUE);
    toRotate.setPixel(0, 1, Color.RED);
    Bitmap zero = TransformationUtils.rotateImage(toRotate, 0);
    assertTrue(toRotate == zero);

    Bitmap ninety = TransformationUtils.rotateImage(toRotate, 90);
    // Checks if native graphics is enabled.
    if (System.getProperty("robolectric.graphicsMode", "").equals("NATIVE")) {
      assertThat(ninety.getPixel(0, 0)).isEqualTo(Color.RED);
      assertThat(ninety.getPixel(1, 0)).isEqualTo(Color.BLUE);
    } else {
      // Use legacy shadow APIs
      assertThat(Shadows.shadowOf(ninety).getDescription()).contains("rotate=90.0");
    }
    assertEquals(toRotate.getWidth(), toRotate.getHeight());
  }

  @Test
  public void testRotateImageExifReturnsGivenBitmapIfRotationIsNormal() {
    Bitmap toRotate = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444);
    // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want
    // not just an identical Bitmap, but our original Bitmap object back.
    Bitmap rotated =
        TransformationUtils.rotateImageExif(bitmapPool, toRotate, ExifInterface.ORIENTATION_NORMAL);
    assertTrue(toRotate == rotated);
  }

  @Test
  public void testRotateImageExifReturnsGivenBitmapIfRotationIsUndefined() {
    Bitmap toRotate = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
    // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want
    // not just an identical Bitmap, but our original Bitmap object back.
    Bitmap rotated =
        TransformationUtils.rotateImageExif(
            bitmapPool, toRotate, ExifInterface.ORIENTATION_UNDEFINED);
    assertTrue(toRotate == rotated);
  }

  @Test
  public void testRotateImageExifReturnsGivenBitmapIfOrientationIsInvalid() {
    Bitmap toRotate = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
    // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want
    // not just an identical Bitmap, but our original Bitmap object back.
    Bitmap rotated = TransformationUtils.rotateImageExif(bitmapPool, toRotate, -1);
    assertTrue(toRotate == rotated);
  }

  @Test
  @Config(sdk = 19)
  public void testRotateImageExif_preservesitmapsWithNullConfigs() {
    Bitmap toRotate = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
    toRotate.setConfig(null);
    Bitmap rotated =
        TransformationUtils.rotateImageExif(
            bitmapPool, toRotate, ExifInterface.ORIENTATION_ROTATE_180);
    assertNull(rotated.getConfig());
  }

  @Test
  @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
  public void rotateImageExif_preservesColorSpace() {
    Bitmap toRotate = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
    toRotate.setColorSpace(ColorSpace.get(ColorSpace.Named.DISPLAY_P3));

    Bitmap rotated =
        TransformationUtils.rotateImageExif(
            bitmapPool, toRotate, ExifInterface.ORIENTATION_ROTATE_90);

    assertEquals(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), rotated.getColorSpace());
  }

  // TODO: Add gainmap-based tests once Robolectric has sufficient support.

  @Test
  public void testInitializeMatrixSetsScaleIfFlipHorizontal() {
    Matrix matrix = mock(Matrix.class);
    TransformationUtils.initializeMatrixForRotation(
        ExifInterface.ORIENTATION_FLIP_HORIZONTAL, matrix);
    verify(matrix).setScale(-1, 1);
  }

  @Test
  public void testInitializeMatrixSetsScaleAndRotateIfFlipVertical() {
    Matrix matrix = mock(Matrix.class);
    TransformationUtils.initializeMatrixForRotation(
        ExifInterface.ORIENTATION_FLIP_VERTICAL, matrix);
    verify(matrix).setRotate(180);
    verify(matrix).postScale(-1, 1);
  }

  @Test
  public void testInitializeMatrixSetsScaleAndRotateIfTranspose() {
    Matrix matrix = mock(Matrix.class);
    TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_TRANSPOSE, matrix);
    verify(matrix).setRotate(90);
    verify(matrix).postScale(-1, 1);
  }

  @Test
  public void testInitializeMatrixSetsScaleAndRotateIfTransverse() {
    Matrix matrix = mock(Matrix.class);
    TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_TRANSVERSE, matrix);
    verify(matrix).setRotate(-90);
    verify(matrix).postScale(-1, 1);
  }

  @Test
  public void testInitializeMatrixSetsRotateOnRotation() {
    Matrix matrix = mock(Matrix.class);
    TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_90, matrix);
    verify(matrix).setRotate(90);
    TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_180, matrix);
    verify(matrix).setRotate(180);
    TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_270, matrix);
    verify(matrix).setRotate(-90);
  }
}