Improved control of downscaling inside JPEG Loader 94/38794/3
authorAndrew Cox <andrew.cox@partner.samsung.com>
Tue, 28 Apr 2015 18:57:04 +0000 (19:57 +0100)
committerAndrew Cox <andrew.cox@partner.samsung.com>
Wed, 6 May 2015 15:44:25 +0000 (16:44 +0100)
* Avoids blurry JPEGs.
* Requires downscaling to be requested by app for jpeg loading library's
  downscaling to be engaged.

Change-Id: I105f243e0bb18fec197c7d5a76f98e71cffd4c98
Signed-off-by: Andrew Cox <andrew.cox@partner.samsung.com>
platform-abstractions/portable/image-operations.cpp
platform-abstractions/portable/image-operations.h
platform-abstractions/tizen/image-loaders/loader-jpeg-turbo.cpp

index bf6ff0b..cb91949 100644 (file)
@@ -149,40 +149,6 @@ inline void DebugAssertDualScanlineParameters( const uint8_t * const scanline1,
 }
 
 /**
- * @brief Work out the desired width and height according to the rules documented for the ImageAttributes class.
- *
- * @param[in] bitmapWidth Width of image before processing.
- * @param[in] bitmapHeight Height of image before processing.
- * @param[in] requestedWidth Width of area to scale image into. Can be zero.
- * @param[in] requestedHeight Height of area to scale image into. Can be zero.
- * @return Dimensions of area to scale image into after special rules are applied.
- * @see ImageAttributes
- */
-ImageDimensions CalculateDesiredDimensions( unsigned int bitmapWidth, unsigned int bitmapHeight, unsigned int requestedWidth, unsigned int requestedHeight )
-{
-  // If no dimensions have been requested, default to the source ones:
-  if( requestedWidth == 0 && requestedHeight == 0 )
-  {
-    return ImageDimensions( bitmapWidth, bitmapHeight );
-  }
-
-  // If both dimensions have values requested, use them both:
-  if( requestedWidth != 0 && requestedHeight != 0 )
-  {
-    return ImageDimensions( requestedWidth, requestedHeight );
-  }
-
-  // If only one of the dimensions has been requested, calculate the other from
-  // the requested one and the source image aspect ratio:
-
-  if( requestedWidth != 0 )
-  {
-    return ImageDimensions( requestedWidth, bitmapHeight / float(bitmapWidth) * requestedWidth + 0.5f );
-  }
-  return ImageDimensions( bitmapWidth / float(bitmapHeight) * requestedHeight + 0.5f, requestedHeight );
-}
-
-/**
  * @brief Converts a scaling mode to the definition of which dimensions matter when box filtering as a part of that mode.
  */
 BoxDimensionTest DimensionTestForScalingMode( FittingMode::Type fittingMode )
@@ -231,7 +197,6 @@ BoxDimensionTest DimensionTestForScalingMode( FittingMode::Type fittingMode )
  */
 ImageDimensions FitForShrinkToFit( ImageDimensions target, ImageDimensions source )
 {
-  DALI_ASSERT_DEBUG( true && " " );
   // Scale the input by the least extreme of the two dimensions:
   const float widthScale  = target.GetX() / float(source.GetX());
   const float heightScale = target.GetY() / float(source.GetY());
@@ -249,9 +214,9 @@ ImageDimensions FitForShrinkToFit( ImageDimensions target, ImageDimensions sourc
 /**
  * @brief Work out the dimensions for a uniform scaling of the input to map it
  * into the target while effecting SCALE_TO_FILL scaling mode.
- * @note The output dimensions will need either top and bottom or left and right
- * to be cropped away unless the source was pre-cropped to match the destination
- * aspect ratio.
+ * @note An image scaled into the output dimensions will need either top and
+ * bottom or left and right to be cropped away unless the source was pre-cropped
+ * to match the destination aspect ratio.
  */
 ImageDimensions FitForScaleToFill( ImageDimensions target, ImageDimensions source )
 {
@@ -367,8 +332,45 @@ BitmapPtr MakeBitmap( const uint8_t * const pixels, Pixel::Format pixelFormat, u
   return newBitmap;
 }
 
+/**
+ * @brief Work out the desired width and height, accounting for zeros.
+ *
+ * @param[in] bitmapWidth Width of image before processing.
+ * @param[in] bitmapHeight Height of image before processing.
+ * @param[in] requestedWidth Width of area to scale image into. Can be zero.
+ * @param[in] requestedHeight Height of area to scale image into. Can be zero.
+ * @return Dimensions of area to scale image into after special rules are applied.
+ */
+ImageDimensions CalculateDesiredDimensions( unsigned int bitmapWidth, unsigned int bitmapHeight, unsigned int requestedWidth, unsigned int requestedHeight )
+{
+  // If no dimensions have been requested, default to the source ones:
+  if( requestedWidth == 0 && requestedHeight == 0 )
+  {
+    return ImageDimensions( bitmapWidth, bitmapHeight );
+  }
+
+  // If both dimensions have values requested, use them both:
+  if( requestedWidth != 0 && requestedHeight != 0 )
+  {
+    return ImageDimensions( requestedWidth, requestedHeight );
+  }
+
+  // Only one of the dimensions has been requested. Calculate the other from
+  // the requested one and the source image aspect ratio:
+  if( requestedWidth != 0 )
+  {
+    return ImageDimensions( requestedWidth, bitmapHeight / float(bitmapWidth) * requestedWidth + 0.5f );
+  }
+  return ImageDimensions( bitmapWidth / float(bitmapHeight) * requestedHeight + 0.5f, requestedHeight );
+}
+
 } // namespace - unnamed
 
+ImageDimensions CalculateDesiredDimensions( ImageDimensions rawDimensions, ImageDimensions requestedDimensions )
+{
+  return CalculateDesiredDimensions( rawDimensions.GetWidth(), rawDimensions.GetHeight(), requestedDimensions.GetWidth(), requestedDimensions.GetHeight() ) ;
+}
+
 /**
  * @brief Implement ScaleTofill scaling mode cropping.
  *
index 6d76cfe..2deb110 100644 (file)
@@ -51,6 +51,16 @@ enum BoxDimensionTest
 typedef Uint16Pair ImageDimensions;
 
 /**
+ * @brief Work out the true desired width and height, accounting for special
+ * rules for zeros in either or both input requested dimensions.
+ *
+ * @param[in] rawDimensions Width and height of image before processing.
+ * @param[in] requestedDimensions Width and height of area to scale image into. Can be zero.
+ * @return Dimensions of area to scale image into after special rules are applied.
+ */
+ImageDimensions CalculateDesiredDimensions( ImageDimensions rawDimensions, ImageDimensions requestedDimensions );
+
+/**
  * @defgroup BitmapOperations Bitmap-to-Bitmap Image operations.
  * @{
  */
index a18e45a..1e41437 100755 (executable)
@@ -21,6 +21,7 @@
 #include <dali/integration-api/bitmap.h>
 #include <resource-loader/debug/resource-loader-debug.h>
 #include "platform-capabilities.h"
+#include "image-operations.h"
 
 // EXTERNAL HEADERS
 #include <libexif/exif-data.h>
@@ -31,7 +32,6 @@
 #include <cstring>
 #include <setjmp.h>
 
-
 namespace Dali
 {
 using Integration::Bitmap;
@@ -168,7 +168,9 @@ bool JpegRotate90 (unsigned char *buffer, int width, int height, int bpp);
 bool JpegRotate180(unsigned char *buffer, int width, int height, int bpp);
 bool JpegRotate270(unsigned char *buffer, int width, int height, int bpp);
 JPGFORM_CODE ConvertExifOrientation(ExifData* exifData);
-bool TransformSize( int requiredWidth, int requiredHeight, JPGFORM_CODE transform,
+bool TransformSize( int requiredWidth, int requiredHeight,
+                    FittingMode::Type fittingMode, SamplingMode::Type samplingMode,
+                    JPGFORM_CODE transform,
                     int& preXformImageWidth, int& preXformImageHeight,
                     int& postXformImageWidth, int& postXformImageHeight );
 
@@ -314,7 +316,10 @@ bool LoadBitmapFromJpeg( const ResourceLoadingClient& client, const ImageLoader:
   int scaledPostXformWidth  = postXformImageWidth;
   int scaledPostXformHeight = postXformImageHeight;
 
-  TransformSize( requiredWidth, requiredHeight, transform,
+  TransformSize( requiredWidth, requiredHeight,
+                 input.scalingParameters.scalingMode,
+                 input.scalingParameters.samplingMode,
+                 transform,
                  scaledPreXformWidth, scaledPreXformHeight,
                  scaledPostXformWidth, scaledPostXformHeight );
 
@@ -325,8 +330,7 @@ bool LoadBitmapFromJpeg( const ResourceLoadingClient& client, const ImageLoader:
   // Allow early cancellation before decoding:
   client.InterruptionPoint();
 
-  const int pitch = scaledPreXformWidth * DECODED_PIXEL_SIZE;
-  if( tjDecompress2( autoJpg.GetHandle(), jpegBufferPtr, jpegBufferSize, bitmapPixelBuffer, scaledPreXformWidth, pitch, scaledPreXformHeight, DECODED_PIXEL_LIBJPEG_TYPE, flags ) == -1 )
+  if( tjDecompress2( autoJpg.GetHandle(), jpegBufferPtr, jpegBufferSize, bitmapPixelBuffer, scaledPreXformWidth, 0, scaledPreXformHeight, DECODED_PIXEL_LIBJPEG_TYPE, flags ) == -1 )
   {
     DALI_LOG_ERROR("%s\n", tjGetErrorStr());
     return false;
@@ -344,19 +348,12 @@ bool LoadBitmapFromJpeg( const ResourceLoadingClient& client, const ImageLoader:
   bool result = false;
   switch(transform)
   {
-    case JPGFORM_FLIP_H:
-    case JPGFORM_FLIP_V:
-    case JPGFORM_TRANSPOSE:
-    case JPGFORM_TRANSVERSE: ///!ToDo: None of these are supported!
-    {
-      DALI_LOG_WARNING( "Unsupported JPEG Orientation transformation: %x.\n", transform );
-      break;
-    }
     case JPGFORM_NONE:
     {
       result = true;
       break;
     }
+    // 3 orientation changes for a camera held perpendicular to the ground or upside-down:
     case JPGFORM_ROT_180:
     {
       result = JpegRotate180(bitmapPixelBuffer, bufferWidth, bufferHeight, DECODED_PIXEL_SIZE);
@@ -372,6 +369,16 @@ bool LoadBitmapFromJpeg( const ResourceLoadingClient& client, const ImageLoader:
       result = JpegRotate90(bitmapPixelBuffer, bufferWidth, bufferHeight, DECODED_PIXEL_SIZE);
       break;
     }
+    /// Less-common orientation changes, since they don't correspond to a camera's
+    // physical orientation:
+    case JPGFORM_FLIP_H:
+    case JPGFORM_FLIP_V:
+    case JPGFORM_TRANSPOSE:
+    case JPGFORM_TRANSVERSE:
+    {
+      DALI_LOG_WARNING( "Unsupported JPEG Orientation transformation: %x.\n", transform );
+      break;
+    }
   }
   return result;
 }
@@ -638,7 +645,9 @@ JPGFORM_CODE ConvertExifOrientation(ExifData* exifData)
   return transform;
 }
 
-bool TransformSize( int requiredWidth, int requiredHeight, JPGFORM_CODE transform,
+bool TransformSize( int requiredWidth, int requiredHeight,
+                    FittingMode::Type fittingMode, SamplingMode::Type samplingMode,
+                    JPGFORM_CODE transform,
                     int& preXformImageWidth, int& preXformImageHeight,
                     int& postXformImageWidth, int& postXformImageHeight )
 {
@@ -650,6 +659,11 @@ bool TransformSize( int requiredWidth, int requiredHeight, JPGFORM_CODE transfor
     std::swap( postXformImageWidth, postXformImageHeight );
   }
 
+  // Apply the special rules for when there are one or two zeros in requested dimensions:
+  const ImageDimensions correctedDesired = Internal::Platform::CalculateDesiredDimensions( ImageDimensions( postXformImageWidth, postXformImageHeight), ImageDimensions( requiredWidth, requiredHeight ) );
+  requiredWidth = correctedDesired.GetWidth();
+  requiredHeight = correctedDesired.GetHeight();
+
   // Rescale image during decode using one of the decoder's built-in rescaling
   // ratios (expected to be powers of 2), keeping the final image at least as
   // wide and high as was requested:
@@ -663,35 +677,79 @@ bool TransformSize( int requiredWidth, int requiredHeight, JPGFORM_CODE transfor
   }
   else
   {
-    // Find nearest supported scaling factor (factors are in sequential order, getting smaller)
-    int scaleFactorIndex( 0 );
-    for( int i = 1; i < numFactors; ++i )
+    // Internal jpeg downscaling is the same as our BOX_X sampling modes so only
+    // apply it if the application requested one of those:
+    // (use a switch case here so this code will fail to compile if other modes are added)
+    bool downscale = true;
+    switch( samplingMode )
     {
-      // if requested width or height set to 0, ignore value
-      // TJSCALED performs an integer-based ceil operation on (dim*factor)
-      if( (requiredWidth  && TJSCALED(postXformImageWidth , (factors[i])) > requiredWidth) ||
-          (requiredHeight && TJSCALED(postXformImageHeight, (factors[i])) > requiredHeight) )
+      case SamplingMode::BOX:
+      case SamplingMode::BOX_THEN_NEAREST:
+      case SamplingMode::BOX_THEN_LINEAR:
+      case SamplingMode::DONT_CARE:
       {
-        scaleFactorIndex = i;
+        downscale = true;
+        break;
       }
-      else
+      case SamplingMode::NO_FILTER:
+      case SamplingMode::NEAREST:
+      case SamplingMode::LINEAR:
       {
-        // This scaling would result in an image that was smaller than requested in both
-        // dimensions, so stop at the previous entry.
+        downscale = false;
         break;
       }
     }
 
-    // Workaround for libjpeg-turbo problem adding a green line on one edge
-    // when downscaling to 1/8 in each dimension. Prefer not to scale to less than
-    // 1/4 in each dimension:
-    if( 2 < scaleFactorIndex )
+    int scaleFactorIndex( 0 );
+    if( downscale )
     {
-      scaleFactorIndex = 2;
-      DALI_LOG_INFO( gLoaderFilter, Debug::General, "Down-scaling requested for image limited to 1/4.\n" );
+      // Find nearest supported scaling factor (factors are in sequential order, getting smaller)
+      for( int i = 1; i < numFactors; ++i )
+      {
+        bool widthLessRequired  = TJSCALED( postXformImageWidth,  factors[i]) < requiredWidth;
+        bool heightLessRequired = TJSCALED( postXformImageHeight, factors[i]) < requiredHeight;
+        switch( fittingMode )
+        {
+          // If either scaled dimension is smaller than the desired one, we were done at the last iteration:
+          case FittingMode::SCALE_TO_FILL:
+          {
+            if ( widthLessRequired || heightLessRequired )
+            {
+              break;
+            }
+          }
+          // If both dimensions are smaller than the desired one, we were done at the last iteration:
+          case FittingMode::SHRINK_TO_FIT:
+          {
+            if ( widthLessRequired && heightLessRequired )
+            {
+              break;
+            }
+          }
+          // If the width is smaller than the desired one, we were done at the last iteration:
+          case FittingMode::FIT_WIDTH:
+          {
+            if ( widthLessRequired )
+            {
+              break;
+            }
+          }
+          // If the width is smaller than the desired one, we were done at the last iteration:
+          case FittingMode::FIT_HEIGHT:
+          {
+            if ( heightLessRequired )
+            {
+              break;
+            }
+          }
+
+          // This factor stays is within our fitting mode constraint so remember it:
+          scaleFactorIndex = i;
+        }
+      }
     }
 
-    // Regardless of requested size, downscale to avoid exceeding the maximum texture size
+    // Regardless of requested size, downscale to avoid exceeding the maximum texture size:
     for( int i = scaleFactorIndex; i < numFactors; ++i )
     {
       // Continue downscaling to below maximum texture size (if possible)
@@ -785,7 +843,7 @@ bool LoadJpegHeader( const ImageLoader::Input& input, unsigned int& width, unsig
         int postXformImageWidth = headerWidth;
         int postXformImageHeight = headerHeight;
 
-        success = TransformSize(requiredWidth, requiredHeight, transform, preXformImageWidth, preXformImageHeight, postXformImageWidth, postXformImageHeight);
+        success = TransformSize( requiredWidth, requiredHeight, input.scalingParameters.scalingMode, input.scalingParameters.samplingMode, transform, preXformImageWidth, preXformImageHeight, postXformImageWidth, postXformImageHeight );
         if(success)
         {
           width = postXformImageWidth;