Implement image fitting modes 58/52858/6
authorTom Robinson <tom.robinson@samsung.com>
Thu, 26 Nov 2015 17:47:23 +0000 (17:47 +0000)
committerTom Robinson <tom.robinson@samsung.com>
Mon, 7 Dec 2015 16:26:06 +0000 (16:26 +0000)
Change-Id: Ia56e4d03b86f1db8d37569a7a0488d702f156f53

automated-tests/src/dali-platform-abstraction/CMakeLists.txt
automated-tests/src/dali-platform-abstraction/utc-image-fitting-modes.cpp [new file with mode: 0644]
platform-abstractions/portable/image-operations.cpp

index b5a8c33..96e6e79 100644 (file)
@@ -6,9 +6,10 @@ SET(RPM_NAME "core-${PKG_NAME}-tests")
 SET(CAPI_LIB "dali-platform-abstraction")
 
 SET(TC_SOURCES
-    utc-image-loading-load-completion.cpp
+    utc-image-fitting-modes.cpp
     utc-image-loading-cancel-all-loads.cpp
     utc-image-loading-cancel-some-loads.cpp
+    utc-image-loading-load-completion.cpp
 )
 
 LIST(APPEND TC_SOURCES
diff --git a/automated-tests/src/dali-platform-abstraction/utc-image-fitting-modes.cpp b/automated-tests/src/dali-platform-abstraction/utc-image-fitting-modes.cpp
new file mode 100644 (file)
index 0000000..9da0cbe
--- /dev/null
@@ -0,0 +1,352 @@
+/*
+ * Copyright (c) 2015 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include "utc-image-loading-common.h"
+
+#include "platform-abstractions/portable/image-operations.h"
+
+#define ANSI_BLACK   "\x1B[0m"
+#define ANSI_RED     "\x1B[31m"
+#define ANSI_GREEN   "\x1B[32m"
+#define ANSI_YELLOW  "\x1B[33m"
+#define ANSI_BLUE    "\x1B[34m"
+#define ANSI_MAGENTA "\x1B[35m"
+#define ANSI_CYAN    "\x1B[36m"
+#define ANSI_WHITE   "\x1B[37m"
+#define ANSI_RESET   "\033[0m"
+
+const unsigned char BORDER_FILL_VALUE = 0xff;
+const char* ASCII_FILL_VALUE = ANSI_YELLOW "#";
+const char* ASCII_PAD_VALUE = ANSI_BLUE "#";
+typedef unsigned char PixelBuffer;
+
+
+void FillBitmap( BitmapPtr bitmap )
+{
+  // Fill the given bitmap fully.
+  const Pixel::Format pixelFormat = bitmap->GetPixelFormat();
+  const unsigned int bytesPerPixel = Pixel::GetBytesPerPixel( pixelFormat );
+  PixelBuffer * const targetPixels = bitmap->GetBuffer();
+  const int bytesToFill = bitmap.Get()->GetImageWidth() * bitmap.Get()->GetImageHeight() * bytesPerPixel;
+
+  memset( targetPixels, BORDER_FILL_VALUE, bytesToFill );
+}
+
+typedef Rect< int > ActiveArea;
+
+// This struct defines all information for one test.
+struct ImageFittingTestParameters
+{
+  unsigned int sourceWidth;
+  unsigned int sourceHeight;
+  unsigned int desiredWidth;
+  unsigned int desiredHeight;
+  FittingMode::Type fittingMode;
+
+  unsigned int expectedWidth;
+  unsigned int expectedHeight;
+  ActiveArea expectedActiveImageArea;
+
+  ImageFittingTestParameters( unsigned int newSourceWidth, unsigned int newSourceHeight, unsigned int newDesiredWidth, unsigned int newDesiredHeight, FittingMode::Type newFittingMode,
+      unsigned int newExpectedWidth, unsigned int newExpectedHeight, ActiveArea newExpectedActiveImageArea )
+  : sourceWidth( newSourceWidth ),
+    sourceHeight( newSourceHeight ),
+    desiredWidth( newDesiredWidth ),
+    desiredHeight( newDesiredHeight ),
+    fittingMode( newFittingMode ),
+    expectedWidth( newExpectedWidth ),
+    expectedHeight( newExpectedHeight ),
+    expectedActiveImageArea( newExpectedActiveImageArea )
+  {
+  }
+};
+
+typedef std::vector< ImageFittingTestParameters > TestContainer;
+
+
+void PerformFittingTests( TestContainer& tests )
+{
+  // Iterate through all pre-defined tests.
+  for( unsigned int testNumber = 0; testNumber < tests.size(); ++testNumber )
+  {
+    // Gather info for this test.
+    ImageFittingTestParameters &test = tests[ testNumber ];
+
+    unsigned int sourceWidth = test.sourceWidth;
+    unsigned int sourceHeight = test.sourceHeight;
+    unsigned int desiredWidth = test.desiredWidth;
+    unsigned int desiredHeight = test.desiredHeight;
+    FittingMode::Type fittingMode = test.fittingMode;
+
+    // Create a source bitmap.
+    ImageDimensions desiredDimensions( desiredWidth, desiredHeight );
+    SamplingMode::Type samplingMode = SamplingMode::BOX_THEN_LINEAR;
+    BitmapPtr sourceBitmap = Integration::Bitmap::New( Integration::Bitmap::BITMAP_2D_PACKED_PIXELS, ResourcePolicy::OWNED_DISCARD );
+    Integration::Bitmap::PackedPixelsProfile *packedView = sourceBitmap->GetPackedPixelsProfile();
+    const Pixel::Format pixelFormat = sourceBitmap->GetPixelFormat();
+    packedView->ReserveBuffer( pixelFormat, sourceWidth, sourceHeight, sourceWidth, sourceHeight );
+
+    // Completely fill the source bitmap (with white).
+    FillBitmap( sourceBitmap );
+
+    // Perform fitting operations (this is the method we are testing).
+    BitmapPtr newBitmap = ApplyAttributesToBitmap( sourceBitmap, desiredDimensions, fittingMode, samplingMode );
+
+    DALI_TEST_CHECK( newBitmap );
+
+    // As we do not need performance within this test, we branch to exit here (for readability, maintainability).
+    if( !newBitmap )
+    {
+      return;
+    }
+
+    Bitmap *bitmap = newBitmap.Get();
+
+    unsigned int resultWidth = bitmap->GetImageWidth();
+    unsigned int resultHeight = bitmap->GetImageHeight();
+
+    // Check the dimensions of the modified image match against the expected values defined in the test.
+    DALI_TEST_EQUALS( resultWidth, test.expectedWidth, TEST_LOCATION );
+    DALI_TEST_EQUALS( resultHeight, test.expectedHeight, TEST_LOCATION );
+
+    PixelBuffer* resultBuffer = bitmap->GetBuffer();
+    const unsigned int bytesPerPixel = Pixel::GetBytesPerPixel( pixelFormat );
+
+    // We generate an ASCII representation of the source, desired and result images to log, purely as a debugging aid.
+    // (0 = border, 1 = active image area - from the source image).
+    std::string xSourceImageString( sourceWidth, '#' );
+    std::string xDesiredSizeString( desiredWidth - 2, '-' );
+    std::string xDesiredSizePadString( desiredWidth - 2, ' ' );
+    tet_printf( "%sRunning test: %d%s\n", ANSI_RED, testNumber + 1, ANSI_RESET );
+    tet_printf( "Source image: %s%s%s\n", ANSI_YELLOW, xSourceImageString.c_str(), ANSI_RESET );
+    for( unsigned int i = 0; i < sourceHeight - 1; ++i )
+    {
+      tet_printf( "              %s%s%s\n", ANSI_YELLOW, xSourceImageString.c_str(), ANSI_RESET );
+    }
+    tet_printf( "Desired size: %s+%s+%s\n", ANSI_YELLOW, xDesiredSizeString.c_str(), ANSI_RESET );
+    for( unsigned int i = 0; i < desiredHeight - 2; ++i )
+    {
+      tet_printf( "              %s|%s|%s\n", ANSI_YELLOW, xDesiredSizePadString.c_str(), ANSI_RESET );
+    }
+    tet_printf( "              %s+%s+%s\n", ANSI_YELLOW, xDesiredSizeString.c_str(), ANSI_RESET );
+
+    // We want to calculate the active image area (the area filled with image data as opposed to borders).
+    // This is so we can determine if the fitting modes worked correctly.
+    ActiveArea resultActiveArea( -1, -1, -1, -1 );
+
+    // Iterate over the result image data to find the active area.
+    for( unsigned int y = 0; y < resultHeight; ++y )
+    {
+      int activeStartX = -1;
+      int activeEndX = -1;
+      std::string xResultImageString;
+
+      for( unsigned int x = 0; x < resultWidth; ++x )
+      {
+        bool pixelPopulated = resultBuffer[ x * bytesPerPixel ] != 0x00;
+
+        // If the pixel is filled AND we haven't found a filled pixel yet,
+        // this is the horizontal start of the active pixel area (for this line).
+        if( pixelPopulated && ( activeStartX == -1 ) )
+        {
+          activeStartX = x;
+        }
+        else if( !pixelPopulated && ( activeStartX != -1 ) && ( activeEndX == -1 ) )
+        {
+          // If the pixel is NOT filled AND we HAVE rpeviously found a filled pixel,
+          // then this is the horizontal end of the active pixel area (for this line).
+          activeEndX = x + 1;
+        }
+
+        // Populate a string with the filled state of the result pixels, to facilitate debugging.
+        xResultImageString += pixelPopulated ? ASCII_FILL_VALUE : ASCII_PAD_VALUE;
+      }
+
+      // First calculate the X-end span value, if we ran out of image before reaching the end of active image area.
+      if( ( activeStartX != -1 ) && ( activeEndX == -1 ) )
+      {
+        activeEndX = resultWidth - activeStartX;
+      }
+
+      // If the X-start pixel on this line is earlier than other lines, the overall active area starts earlier.
+      // Note: This is ignored if there was no pixels found.
+      if( ( activeStartX != -1 ) && ( ( activeStartX < resultActiveArea.x ) || ( resultActiveArea.x == -1 ) ) )
+      {
+        resultActiveArea.x = activeStartX;
+      }
+
+      // If the X-end pixel on this line is later than other lines, the overall active area starts later.
+      // Note: This is ignored if there was no pixels found.
+      if( ( activeEndX != -1 ) && ( ( activeEndX > resultActiveArea.width ) || ( resultActiveArea.width == -1 ) ) )
+      {
+        resultActiveArea.width = activeEndX;
+      }
+
+      // If there was an X-start pixel on this line AND we don't yet have a Y-start, this line IS the Y-start.
+      if( ( activeStartX != -1 ) && ( resultActiveArea.y == -1 ) )
+      {
+        resultActiveArea.y = y;
+      }
+
+      // If there was no X-start pixel on this line AND we already have a Y-start value,
+      // then the last Y becomes the new Y-end value.
+      if( ( activeStartX == -1 ) && ( resultActiveArea.y != -1 ) && ( resultActiveArea.height == -1 ) )
+      {
+        resultActiveArea.height = y - 1;
+      }
+
+      if( y == 0 )
+      {
+        tet_printf( "Result image: %s\n", xResultImageString.c_str() );
+      }
+      else
+      {
+        tet_printf( "              %s\n", xResultImageString.c_str() );
+      }
+
+      resultBuffer += resultWidth * bytesPerPixel;
+    }
+
+    // Calculate the Y-end value, if we ran out of image before reaching the end of active image area.
+    if( ( resultActiveArea.y != -1 ) && ( resultActiveArea.height == -1 ) )
+    {
+      resultActiveArea.height = resultHeight - resultActiveArea.y;
+    }
+
+    tet_printf( "%s", ANSI_RESET );
+    tet_printf( "Test: %d  Result image dimensions: %d,%d  ActiveArea: %d,%d,%d,%d\n",
+        testNumber + 1, resultWidth, resultHeight, resultActiveArea.x, resultActiveArea.y, resultActiveArea.width, resultActiveArea.height );
+
+    // Test the result images active area matches the expected active area defined in the test.
+    DALI_TEST_EQUALS( resultActiveArea, test.expectedActiveImageArea, TEST_LOCATION );
+  }
+}
+
+// Test cases:
+
+// Positive test case for fitting mode: FIT_WIDTH.
+int UtcDaliFittingModesFitWidth(void)
+{
+  tet_printf("Running fitting mode test for: FIT_WIDTH\n");
+
+  TestContainer tests;
+
+  // Here we can define the input and expected output of each test on a single line.
+  // Source Width, Source Height, Desired Width, Desired Height, Fitting Mode, Expected Width, Expected Height, ActiveArea: X-start, Y-start, width, height
+
+  // Test Image source size = desired size. Output should be the same.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 4, 4, FittingMode::FIT_WIDTH,     4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect same. Should scale size down.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 2, 2, FittingMode::FIT_WIDTH,     2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect same. Should not scale size up.
+  tests.push_back( ImageFittingTestParameters( 2, 2, 4, 4, FittingMode::FIT_WIDTH,     2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect different. Should crop height, so no borders. No scale up as result has same aspect after crop.
+  tests.push_back( ImageFittingTestParameters( 2, 4, 8, 8, FittingMode::FIT_WIDTH,     2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size > desired size, but aspect different (w < h). Should crop height, so no borders. No scale as result is same size as desired size.
+  tests.push_back( ImageFittingTestParameters( 4, 8, 4, 4, FittingMode::FIT_WIDTH,     4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w > h). Should add borders, AND scale down to desired size.
+  tests.push_back( ImageFittingTestParameters( 8, 4, 4, 4, FittingMode::FIT_WIDTH,     4, 4, ActiveArea( 0, 1, 4, 2 ) ) );
+
+  PerformFittingTests( tests );
+
+  END_TEST;
+}
+
+// Positive test case for fitting mode: FIT_HEIGHT.
+int UtcDaliFittingModesFitHeight(void)
+{
+  tet_printf("Running fitting mode test for: FIT_HEIGHT\n");
+
+  TestContainer tests;
+
+  // Here we can define the input and expected output of each test on a single line.
+  // Source Width, Source Height, Desired Width, Desired Height, Fitting Mode, Expected Width, Expected Height, ActiveArea: X-start, Y-start, width, height
+
+  // Test Image source size = desired size. Output should be the same.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 4, 4, FittingMode::FIT_HEIGHT,    4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect same. Should scale size down.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 2, 2, FittingMode::FIT_HEIGHT,    2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect same. Should not scale size up.
+  tests.push_back( ImageFittingTestParameters( 2, 2, 4, 4, FittingMode::FIT_HEIGHT,    2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect different. Should add borders, but not scale overall size up.
+  tests.push_back( ImageFittingTestParameters( 2, 4, 8, 8, FittingMode::FIT_HEIGHT,    4, 4, ActiveArea( 1, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w < h). Should add borders, AND scale down to desired size.
+  tests.push_back( ImageFittingTestParameters( 4, 8, 4, 4, FittingMode::FIT_HEIGHT,    4, 4, ActiveArea( 1, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w > h). Should crop width, so no borders. No scale as result is same size as desired size.
+  tests.push_back( ImageFittingTestParameters( 8, 4, 4, 4, FittingMode::FIT_HEIGHT,    4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+
+  PerformFittingTests( tests );
+
+  END_TEST;
+}
+
+// Positive test case for fitting mode: SHRINK_TO_FIT.
+int UtcDaliFittingModesShrinkToFit(void)
+{
+  tet_printf("Running fitting mode test for: SHRINK_TO_FIT\n");
+
+  TestContainer tests;
+
+  // Here we can define the input and expected output of each test on a single line.
+  // Source Width, Source Height, Desired Width, Desired Height, Fitting Mode, Expected Width, Expected Height, ActiveArea: X-start, Y-start, width, height
+
+  // Test Image source size = desired size. Output should be the same.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 4, 4, FittingMode::SHRINK_TO_FIT, 4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect same. Should scale size down.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 2, 2, FittingMode::SHRINK_TO_FIT, 2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect same. Should not scale size up.
+  tests.push_back( ImageFittingTestParameters( 2, 2, 4, 4, FittingMode::SHRINK_TO_FIT, 2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect different. Should add borders, but not scale overall size up, as although image is smaller than desired size, aspect is the same.
+  tests.push_back( ImageFittingTestParameters( 2, 4, 8, 8, FittingMode::SHRINK_TO_FIT, 4, 4, ActiveArea( 1, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w < h). Should add borders, AND scale down to desired size.
+  tests.push_back( ImageFittingTestParameters( 4, 8, 4, 4, FittingMode::SHRINK_TO_FIT, 4, 4, ActiveArea( 1, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w > h). Should add borders, AND scale down to desired size.
+  tests.push_back( ImageFittingTestParameters( 8, 4, 4, 4, FittingMode::SHRINK_TO_FIT, 4, 4, ActiveArea( 0, 1, 4, 2 ) ) );
+
+  PerformFittingTests( tests );
+
+  END_TEST;
+}
+
+// Positive test case for fitting mode: SCALE_TO_FILL.
+int UtcDaliFittingModesScaleToFill(void)
+{
+  tet_printf("Running fitting mode test for: SCALE_TO_FILL\n");
+
+  TestContainer tests;
+
+  // Here we can define the input and expected output of each test on a single line.
+  // Source Width, Source Height, Desired Width, Desired Height, Fitting Mode, Expected Width, Expected Height, ActiveArea: X-start, Y-start, width, height
+
+  // Test Image source size = desired size. Output should be the same.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 4, 4, FittingMode::SCALE_TO_FILL, 4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect same. Should scale size down.
+  tests.push_back( ImageFittingTestParameters( 4, 4, 2, 2, FittingMode::SCALE_TO_FILL, 2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect same. Should not scale size up.
+  tests.push_back( ImageFittingTestParameters( 2, 2, 4, 4, FittingMode::SCALE_TO_FILL, 2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size < desired size, but aspect different. Should crop height, so no borders. No scale up as result has same aspect after crop.
+  tests.push_back( ImageFittingTestParameters( 2, 4, 8, 8, FittingMode::SCALE_TO_FILL, 2, 2, ActiveArea( 0, 0, 2, 2 ) ) );
+  // Test Image source size > desired size, but aspect different (w < h). Should crop height, so no borders. No scale as result is same size as desired size.
+  tests.push_back( ImageFittingTestParameters( 4, 8, 4, 4, FittingMode::SCALE_TO_FILL, 4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+  // Test Image source size > desired size, but aspect different (w > h). Should crop width, so no borders. No scale as result is same size as desired size.
+  tests.push_back( ImageFittingTestParameters( 8, 4, 4, 4, FittingMode::SCALE_TO_FILL, 4, 4, ActiveArea( 0, 0, 4, 4 ) ) );
+
+  PerformFittingTests( tests );
+
+  END_TEST;
+}
+
index fd2e836..9441488 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014 Samsung Electronics Co., Ltd.
+ * Copyright (c) 2015 Samsung Electronics Co., Ltd.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -36,6 +36,13 @@ namespace Platform
 namespace
 {
 
+// The BORDER_FILL_VALUE is a single byte value that is used for horizontal and vertical borders.
+// A value of 0x00 gives us transparency for pixel buffers with an alpha channel, or black otherwise.
+// We can optionally use a Vector4 color here, but at reduced fill speed.
+const uint8_t BORDER_FILL_VALUE( 0x00 );
+// A maximum size limit for newly created bitmaps. ( 1u << 16 ) - 1 is chosen as we are using 16bit words for dimensions.
+const unsigned int MAXIMUM_TARGET_BITMAP_SIZE( ( 1u << 16 ) - 1 );
+
 using Integration::Bitmap;
 using Integration::BitmapPtr;
 typedef unsigned char PixelBuffer;
@@ -305,6 +312,97 @@ ImageDimensions FitToScalingMode( ImageDimensions requestedSize, ImageDimensions
 }
 
 /**
+ * @brief Calculate the number of lines on the X and Y axis that need to be
+ * either added or removed with repect to the specified fitting mode.
+ * (e.g., nearest or linear).
+ * @param[in]     sourceSize      The size of the source image
+ * @param[in]     fittingMode     The fitting mode to use
+ * @param[in/out] requestedSize   The target size that the image will be fitted to.
+ *                                If the source image is smaller than the requested size, the source is not scaled up.
+ *                                So we reduce the target size while keeping aspect by lowering resolution.
+ * @param[out]    scanlinesToCrop The number of scanlines to remove from the image (can be negative to represent Y borders required)
+ * @param[out]    columnsToCrop   The number of columns to remove from the image (can be negative to represent X borders required)
+ */
+void CalculateBordersFromFittingMode(  ImageDimensions sourceSize, FittingMode::Type fittingMode, ImageDimensions& requestedSize, int& scanlinesToCrop, int& columnsToCrop )
+{
+  const unsigned int sourceWidth( sourceSize.GetWidth() );
+  const unsigned int sourceHeight( sourceSize.GetHeight() );
+  const float targetAspect( static_cast< float >( requestedSize.GetWidth() ) / static_cast< float >( requestedSize.GetHeight() ) );
+  int finalWidth = 0;
+  int finalHeight = 0;
+
+  switch( fittingMode )
+  {
+    case FittingMode::FIT_WIDTH:
+    {
+      finalWidth = sourceWidth;
+      finalHeight = static_cast< float >( sourceWidth ) / targetAspect;
+
+      columnsToCrop = 0;
+      scanlinesToCrop = -( finalHeight - sourceHeight );
+      break;
+    }
+
+    case FittingMode::FIT_HEIGHT:
+    {
+      finalWidth = static_cast< float >( sourceHeight ) * targetAspect;
+      finalHeight = sourceHeight;
+
+      columnsToCrop = -( finalWidth - sourceWidth );
+      scanlinesToCrop = 0;
+      break;
+    }
+
+    case FittingMode::SHRINK_TO_FIT:
+    {
+      const float sourceAspect( static_cast< float >( sourceWidth ) / static_cast< float >( sourceHeight ) );
+      if( sourceAspect > targetAspect )
+      {
+        finalWidth = sourceWidth;
+        finalHeight = static_cast< float >( sourceWidth ) / targetAspect;
+
+        columnsToCrop = 0;
+        scanlinesToCrop = -( finalHeight - sourceHeight );
+      }
+      else
+      {
+        finalWidth = static_cast< float >( sourceHeight ) * targetAspect;
+        finalHeight = sourceHeight;
+
+        columnsToCrop = -( finalWidth - sourceWidth );
+        scanlinesToCrop = 0;
+      }
+      break;
+    }
+
+    case FittingMode::SCALE_TO_FILL:
+    {
+      const float sourceAspect( static_cast< float >( sourceWidth ) / static_cast< float >( sourceHeight ) );
+      if( sourceAspect > targetAspect )
+      {
+        finalWidth = static_cast< float >( sourceHeight ) * targetAspect;
+        finalHeight = sourceHeight;
+
+        columnsToCrop = -( finalWidth - sourceWidth );
+        scanlinesToCrop = 0;
+      }
+      else
+      {
+        finalWidth = sourceWidth;
+        finalHeight = static_cast< float >( sourceWidth ) / targetAspect;
+
+        columnsToCrop = 0;
+        scanlinesToCrop = -( finalHeight - sourceHeight );
+      }
+      break;
+    }
+  }
+
+  requestedSize.SetWidth( finalWidth );
+  requestedSize.SetHeight( finalHeight );
+}
+
+/**
  * @brief Construct a bitmap with format and dimensions requested.
  */
 BitmapPtr MakeEmptyBitmap( Pixel::Format pixelFormat, unsigned int width, unsigned int height )
@@ -372,24 +470,36 @@ ImageDimensions CalculateDesiredDimensions( ImageDimensions rawDimensions, Image
 }
 
 /**
- * @brief Implement ScaleTofill scaling mode cropping.
+ * @brief Apply cropping and padding for specified fitting mode.
  *
- * Implement the cropping required for SCALE_TO_FILL mode,
- * returning a new bitmap with the aspect ratio specified by the scaling mode.
- * This scaling mode selects the central portion of a source image so any spare
- * pixels off one of either the top or bottom edge need to be removed.
+ * Once the bitmap has been (optionally) downscaled to an appropriate size, this method performs alterations
+ * based on the fitting mode.
  *
- * @note If the input bitmap was not previously downscaled to exactly encompass
- * the desired output size, the resulting bitmap will have the correct aspect
- * ratio but will have larger dimensions than requested. This can be used to
- * fake the scaling mode by relying on the GPU scaling at render time.
- * If the input bitmap was previously maximally downscaled using a
- * repeated box filter, this is a reasonable approach.
+ * This will add vertical or horizontal borders if necessary.
+ * Crop the source image data vertically or horizontally if necessary.
+ * The aspect of the source image is preserved.
+ * If the source image is smaller than the desired size, the algorithm will modify the the newly created
+ *   bitmaps dimensions to only be as large as necessary, as a memory saving optimization. This will cause
+ *   GPU scaling to be performed at render time giving the same result with less texture traversal.
+ *
+ * @param[in] bitmap            The source bitmap to perform modifications on.
+ * @param[in] desiredDimensions The target dimensions to aim to fill based on the fitting mode.
+ * @param[in] fittingMode       The fitting mode to use.
+ *
+ * @return                      A new bitmap with the padding and cropping required for fitting mode applied.
+ *                              If no modification is needed or possible, the passed in bitmap is returned.
+ */
+Integration::BitmapPtr CropAndPadForFittingMode( Integration::BitmapPtr bitmap, ImageDimensions desiredDimensions, FittingMode::Type fittingMode );
+
+/**
+ * @brief Adds horizontal or vertical borders to the source image.
  *
- * @return The bitmap passed in if no scaling is needed or possible, else a new,
- * smaller bitmap with the cropping required for the scaling mode applied.
+ * @param[in] targetPixels     The destination image pointer to draw the borders on.
+ * @param[in] bytesPerPixel    The number of bytes per pixel of the target pixel buffer.
+ * @param[in] targetDimensions The dimensions of the destination image.
+ * @param[in] padDimensions    The columns and scanlines to pad with borders.
  */
-Integration::BitmapPtr CropForScaleToFill( Integration::BitmapPtr bitmap, ImageDimensions desiredDimensions );
+void AddBorders( PixelBuffer *targetPixels, const unsigned int bytesPerPixel, const ImageDimensions targetDimensions, const ImageDimensions padDimensions );
 
 BitmapPtr ApplyAttributesToBitmap( BitmapPtr bitmap, ImageDimensions dimensions, FittingMode::Type fittingMode, SamplingMode::Type samplingMode )
 {
@@ -404,10 +514,11 @@ BitmapPtr ApplyAttributesToBitmap( BitmapPtr bitmap, ImageDimensions dimensions,
     bitmap = DownscaleBitmap( *bitmap, desiredDimensions, fittingMode, samplingMode );
 
     // Cut the bitmap according to the desired width and height so that the
-    // resulting bitmap has the same aspect ratio as the desired dimensions:
-    if( bitmap && bitmap->GetPackedPixelsProfile() && fittingMode == FittingMode::SCALE_TO_FILL )
+    // resulting bitmap has the same aspect ratio as the desired dimensions.
+    // Add crop and add borders if necessary depending on fitting mode.
+    if( bitmap && bitmap->GetPackedPixelsProfile() )
     {
-      bitmap = CropForScaleToFill( bitmap, desiredDimensions );
+      bitmap = CropAndPadForFittingMode( bitmap, desiredDimensions, fittingMode );
     }
 
     // Examine the image pixels remaining after cropping and scaling to see if all
@@ -421,69 +532,95 @@ BitmapPtr ApplyAttributesToBitmap( BitmapPtr bitmap, ImageDimensions dimensions,
   return bitmap;
 }
 
-BitmapPtr CropForScaleToFill( BitmapPtr bitmap, ImageDimensions desiredDimensions )
+BitmapPtr CropAndPadForFittingMode( BitmapPtr bitmap, ImageDimensions desiredDimensions, FittingMode::Type fittingMode )
 {
-  const unsigned inputWidth = bitmap->GetImageWidth();
-  const unsigned inputHeight = bitmap->GetImageHeight();
-  const unsigned desiredWidth = desiredDimensions.GetWidth();
-  const unsigned desiredHeight = desiredDimensions.GetHeight();
+  const unsigned int inputWidth = bitmap->GetImageWidth();
+  const unsigned int inputHeight = bitmap->GetImageHeight();
 
-  if( desiredWidth < 1U || desiredHeight < 1U )
+  if( desiredDimensions.GetWidth() < 1u || desiredDimensions.GetHeight() < 1u )
   {
-    DALI_LOG_WARNING( "Image scaling aborted as desired dimensions too small (%u, %u)\n.", desiredWidth, desiredHeight );
+    DALI_LOG_WARNING( "Image scaling aborted as desired dimensions too small (%u, %u).\n", desiredDimensions.GetWidth(), desiredDimensions.GetHeight() );
   }
-  else if( inputWidth != desiredWidth || inputHeight != desiredHeight )
+  else if( inputWidth != desiredDimensions.GetWidth() || inputHeight != desiredDimensions.GetHeight() )
   {
-    const Vector2 desiredDims( desiredWidth, desiredHeight );
-
-    // Scale the desired rectangle back to fit inside the rectangle of the loaded bitmap:
-    // There are two candidates (scaled by x, and scaled by y) and we choose the smallest area one.
-    const float widthsRatio = inputWidth / float(desiredWidth);
-    const Vector2 scaledByWidth = desiredDims * widthsRatio;
-    const float heightsRatio = inputHeight / float(desiredHeight);
-    const Vector2 scaledByHeight = desiredDims * heightsRatio;
-    // Trim top and bottom if the area of the horizontally-fitted candidate is less, else trim the sides:
-    const bool trimTopAndBottom = scaledByWidth.width * scaledByWidth.height < scaledByHeight.width * scaledByHeight.height;
-    const Vector2 scaledDims = trimTopAndBottom ? scaledByWidth : scaledByHeight;
-
-    // Work out how many pixels to trim from top and bottom, and left and right:
-    // (We only ever do one dimension)
-    const unsigned scanlinesToTrim = trimTopAndBottom ? fabsf( (scaledDims.y - inputHeight) * 0.5f ) : 0;
-    const unsigned columnsToTrim = trimTopAndBottom ? 0 : fabsf( (scaledDims.x - inputWidth) * 0.5f );
-
-    DALI_LOG_INFO( gImageOpsLogFilter, Debug::Concise, "Bitmap, desired(%f, %f), loaded(%u,%u), cut_target(%f, %f), trimmed(%u, %u), vertical = %s.\n", desiredDims.x, desiredDims.y, inputWidth, inputHeight, scaledDims.x, scaledDims.y, columnsToTrim, scanlinesToTrim, trimTopAndBottom ? "true" : "false" );
-
-    // Make a new bitmap with the central part of the loaded one if required:
-    if( scanlinesToTrim > 0 || columnsToTrim > 0 )
+    // Calculate any padding or cropping that needs to be done based on the fitting mode.
+    // Note: If the desired size is larger than the original image, the desired size will be
+    // reduced while maintaining the aspect, in order to save unnecessary memory usage.
+    int scanlinesToCrop = 0;
+    int columnsToCrop = 0;
+
+    CalculateBordersFromFittingMode( ImageDimensions( inputWidth, inputHeight ), fittingMode, desiredDimensions, scanlinesToCrop, columnsToCrop );
+
+    unsigned int desiredWidth( desiredDimensions.GetWidth() );
+    unsigned int desiredHeight( desiredDimensions.GetHeight() );
+
+    // Action the changes by making a new bitmap with the central part of the loaded one if required.
+    if( scanlinesToCrop != 0 || columnsToCrop != 0 )
     {
-      const unsigned newWidth = inputWidth - 2 * columnsToTrim;
-      const unsigned newHeight = inputHeight - 2 * scanlinesToTrim;
+      // Split the adding and removing of scanlines and columns into separate variables,
+      // so we can use one piece of generic code to action the changes.
+      unsigned int scanlinesToPad = 0;
+      unsigned int columnsToPad = 0;
+      if( scanlinesToCrop < 0 )
+      {
+        scanlinesToPad = -scanlinesToCrop;
+        scanlinesToCrop = 0;
+      }
+      if( columnsToCrop < 0 )
+      {
+        columnsToPad = -columnsToCrop;
+        columnsToCrop = 0;
+      }
+
+      // If there is no filtering, then the final image size can become very large, exit if larger than maximum.
+      if( ( desiredWidth > MAXIMUM_TARGET_BITMAP_SIZE ) || ( desiredHeight > MAXIMUM_TARGET_BITMAP_SIZE ) ||
+          ( columnsToPad > MAXIMUM_TARGET_BITMAP_SIZE ) || ( scanlinesToPad > MAXIMUM_TARGET_BITMAP_SIZE ) )
+      {
+        DALI_LOG_WARNING( "Image scaling aborted as final dimensions too large (%u, %u).\n", desiredWidth, desiredHeight );
+        return bitmap;
+      }
+
+      // Create a new bitmap with the desired size.
       BitmapPtr croppedBitmap = Integration::Bitmap::New( Integration::Bitmap::BITMAP_2D_PACKED_PIXELS, ResourcePolicy::OWNED_DISCARD );
-      Integration::Bitmap::PackedPixelsProfile * packedView = croppedBitmap->GetPackedPixelsProfile();
+      Integration::Bitmap::PackedPixelsProfile *packedView = croppedBitmap->GetPackedPixelsProfile();
       DALI_ASSERT_DEBUG( packedView );
       const Pixel::Format pixelFormat = bitmap->GetPixelFormat();
-      packedView->ReserveBuffer( pixelFormat, newWidth, newHeight, newWidth, newHeight );
-
-      const unsigned bytesPerPixel = Pixel::GetBytesPerPixel( pixelFormat );
-
-      const PixelBuffer * const srcPixels = bitmap->GetBuffer() + scanlinesToTrim * inputWidth * bytesPerPixel;
-      PixelBuffer * const destPixels = croppedBitmap->GetBuffer();
-      DALI_ASSERT_DEBUG( srcPixels && destPixels );
-
-      // Optimize to a single memcpy if the left and right edges don't need a crop, else copy a scanline at a time:
-      if( trimTopAndBottom )
+      packedView->ReserveBuffer( pixelFormat, desiredWidth, desiredHeight, desiredWidth, desiredHeight );
+
+      // Add some pre-calculated offsets to the bitmap pointers so this is not done within a loop.
+      // The cropping is added to the source pointer, and the padding is added to the destination.
+      const unsigned int bytesPerPixel = Pixel::GetBytesPerPixel( pixelFormat );
+      const PixelBuffer * const sourcePixels = bitmap->GetBuffer() + ( ( ( ( scanlinesToCrop / 2 ) * inputWidth ) + ( columnsToCrop / 2 ) ) * bytesPerPixel );
+      PixelBuffer * const targetPixels = croppedBitmap->GetBuffer();
+      PixelBuffer * const targetPixelsActive = targetPixels + ( ( ( ( scanlinesToPad / 2 ) * desiredWidth ) + ( columnsToPad / 2 ) ) * bytesPerPixel );
+      DALI_ASSERT_DEBUG( sourcePixels && targetPixels );
+
+      // Copy the image data to the new bitmap.
+      // Optimize to a single memcpy if the left and right edges don't need a crop or a pad.
+      unsigned int outputSpan( desiredWidth * bytesPerPixel );
+      if( columnsToCrop == 0 && columnsToPad == 0 )
       {
-        memcpy( destPixels, srcPixels, newHeight * newWidth * bytesPerPixel );
+        memcpy( targetPixelsActive, sourcePixels, ( desiredHeight - scanlinesToPad ) * outputSpan );
       }
       else
       {
-        for( unsigned y = 0; y < newHeight; ++y )
+        // The width needs to change (due to either a crop or a pad), so we copy a scanline at a time.
+        // Precalculate any constants to optimize the inner loop.
+        const unsigned int inputSpan( inputWidth * bytesPerPixel );
+        const unsigned int copySpan( ( desiredWidth - columnsToPad ) * bytesPerPixel );
+        const unsigned int scanlinesToCopy( desiredHeight - scanlinesToPad );
+
+        for( unsigned int y = 0; y < scanlinesToCopy; ++y )
         {
-          memcpy( &destPixels[y * newWidth * bytesPerPixel], &srcPixels[y * inputWidth * bytesPerPixel + columnsToTrim * bytesPerPixel], newWidth * bytesPerPixel );
+          memcpy( &targetPixelsActive[ y * outputSpan ], &sourcePixels[ y * inputSpan ], copySpan );
         }
       }
 
-      // Overwrite the loaded bitmap with the cropped version:
+      // Add vertical or horizontal borders to the final image (if required).
+      desiredDimensions.SetWidth( desiredWidth );
+      desiredDimensions.SetHeight( desiredHeight );
+      AddBorders( croppedBitmap->GetBuffer(), bytesPerPixel, desiredDimensions, ImageDimensions( columnsToPad, scanlinesToPad ) );
+      // Overwrite the loaded bitmap with the cropped version
       bitmap = croppedBitmap;
     }
   }
@@ -491,6 +628,54 @@ BitmapPtr CropForScaleToFill( BitmapPtr bitmap, ImageDimensions desiredDimension
   return bitmap;
 }
 
+void AddBorders( PixelBuffer *targetPixels, const unsigned int bytesPerPixel, const ImageDimensions targetDimensions, const ImageDimensions padDimensions )
+{
+  // Assign ints for faster access.
+  unsigned int desiredWidth( targetDimensions.GetWidth() );
+  unsigned int desiredHeight( targetDimensions.GetHeight() );
+  unsigned int columnsToPad( padDimensions.GetWidth() );
+  unsigned int scanlinesToPad( padDimensions.GetHeight() );
+  unsigned int outputSpan( desiredWidth * bytesPerPixel );
+
+  // Add letterboxing (symmetrical borders) if needed.
+  if( scanlinesToPad > 0 )
+  {
+    // Add a top border. Note: This is (deliberately) rounded down if padding is an odd number.
+    memset( targetPixels, BORDER_FILL_VALUE, ( scanlinesToPad / 2 ) * outputSpan );
+
+    // We subtract scanlinesToPad/2 from scanlinesToPad so that we have the correct
+    // offset for odd numbers (as the top border is 1 pixel smaller in these cases.
+    unsigned int bottomBorderHeight = scanlinesToPad - ( scanlinesToPad / 2 );
+
+    // Bottom border.
+    memset( &targetPixels[ ( desiredHeight - bottomBorderHeight ) * outputSpan ], BORDER_FILL_VALUE, bottomBorderHeight * outputSpan );
+  }
+  else if( columnsToPad > 0 )
+  {
+    // Add a left and right border.
+    // Left:
+    // Pre-calculate span size outside of loop.
+    unsigned int leftBorderSpanWidth( ( columnsToPad / 2 ) * bytesPerPixel );
+    for( unsigned int y = 0; y < desiredHeight; ++y )
+    {
+      memset( &targetPixels[ y * outputSpan ], BORDER_FILL_VALUE, leftBorderSpanWidth );
+    }
+
+    // Right:
+    // Pre-calculate the initial x offset as it is always the same for a small optimization.
+    // We subtract columnsToPad/2 from columnsToPad so that we have the correct
+    // offset for odd numbers (as the left border is 1 pixel smaller in these cases.
+    unsigned int rightBorderWidth = columnsToPad - ( columnsToPad / 2 );
+    PixelBuffer * const destPixelsRightBorder( targetPixels + ( ( desiredWidth - rightBorderWidth ) * bytesPerPixel ) );
+    unsigned int rightBorderSpanWidth = rightBorderWidth * bytesPerPixel;
+
+    for( unsigned int y = 0; y < desiredHeight; ++y )
+    {
+      memset( &destPixelsRightBorder[ y * outputSpan ], BORDER_FILL_VALUE, rightBorderSpanWidth );
+    }
+  }
+}
+
 Integration::BitmapPtr DownscaleBitmap( Integration::Bitmap& bitmap,
                                         ImageDimensions desired,
                                         FittingMode::Type fittingMode,