1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/thumbnails/simple_thumbnail_crop.h" 6 7 #include "base/metrics/histogram.h" 8 #include "content/public/browser/browser_thread.h" 9 #include "skia/ext/platform_canvas.h" 10 #include "ui/gfx/color_utils.h" 11 #include "ui/gfx/image/image_skia.h" 12 #include "ui/gfx/screen.h" 13 #include "ui/gfx/scrollbar_size.h" 14 #include "ui/gfx/size_conversions.h" 15 #include "ui/gfx/skbitmap_operations.h" 16 17 namespace { 18 static const char kThumbnailHistogramName[] = "Thumbnail.ComputeMS"; 19 } 20 21 namespace thumbnails { 22 23 SimpleThumbnailCrop::SimpleThumbnailCrop(const gfx::Size& target_size) 24 : target_size_(target_size) { 25 DCHECK(!target_size.IsEmpty()); 26 } 27 28 ClipResult SimpleThumbnailCrop::GetCanvasCopyInfo( 29 const gfx::Size& source_size, 30 ui::ScaleFactor scale_factor, 31 gfx::Rect* clipping_rect, 32 gfx::Size* target_size) const { 33 DCHECK(!source_size.IsEmpty()); 34 ClipResult clip_result = thumbnails::CLIP_RESULT_NOT_CLIPPED; 35 *clipping_rect = GetClippingRect(source_size, target_size_, &clip_result); 36 *target_size = GetCopySizeForThumbnail(scale_factor, target_size_); 37 return clip_result; 38 } 39 40 void SimpleThumbnailCrop::ProcessBitmap( 41 scoped_refptr<ThumbnailingContext> context, 42 const ConsumerCallback& callback, 43 const SkBitmap& bitmap) { 44 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 45 if (bitmap.isNull() || bitmap.empty()) 46 return; 47 48 SkBitmap thumbnail = CreateThumbnail( 49 bitmap, 50 ComputeTargetSizeAtMaximumScale(target_size_), 51 &context->clip_result); 52 53 context->score.boring_score = CalculateBoringScore(thumbnail); 54 context->score.good_clipping = 55 (context->clip_result == CLIP_RESULT_WIDER_THAN_TALL || 56 context->clip_result == CLIP_RESULT_TALLER_THAN_WIDE || 57 context->clip_result == CLIP_RESULT_NOT_CLIPPED); 58 59 callback.Run(*context.get(), thumbnail); 60 } 61 62 double SimpleThumbnailCrop::CalculateBoringScore(const SkBitmap& bitmap) { 63 if (bitmap.isNull() || bitmap.empty()) 64 return 1.0; 65 int histogram[256] = {0}; 66 color_utils::BuildLumaHistogram(bitmap, histogram); 67 68 int color_count = *std::max_element(histogram, histogram + 256); 69 int pixel_count = bitmap.width() * bitmap.height(); 70 return static_cast<double>(color_count) / pixel_count; 71 } 72 73 SkBitmap SimpleThumbnailCrop::GetClippedBitmap(const SkBitmap& bitmap, 74 int desired_width, 75 int desired_height, 76 ClipResult* clip_result) { 77 gfx::Rect clipping_rect = 78 GetClippingRect(gfx::Size(bitmap.width(), bitmap.height()), 79 gfx::Size(desired_width, desired_height), 80 clip_result); 81 SkIRect src_rect = { clipping_rect.x(), clipping_rect.y(), 82 clipping_rect.right(), clipping_rect.bottom() }; 83 SkBitmap clipped_bitmap; 84 bitmap.extractSubset(&clipped_bitmap, src_rect); 85 return clipped_bitmap; 86 } 87 88 // Returns the size used by RenderWidgetHost::CopyFromBackingStore. 89 // 90 // The size is calculated in such a way that the copied size in pixel becomes 91 // equal to (f * kThumbnailWidth, f * kThumbnailHeight), where f is the scale 92 // of ui::SCALE_FACTOR_200P. Since RenderWidgetHost::CopyFromBackingStore takes 93 // the size in DIP, we need to adjust the size based on |view|'s device scale 94 // factor in order to copy the pixels with the size above. 95 // 96 // The copied size was chosen for the following reasons. 97 // 98 // 1. When the scale factor of the primary monitor is ui::SCALE_FACTOR_200P, the 99 // generated thumbnail size is (f * kThumbnailWidth, f * kThumbnailHeight). 100 // In order to avoid degrading the image quality by magnification, the size 101 // of the copied pixels should be equal to or larger than this thumbnail size. 102 // 103 // 2. RenderWidgetHost::CopyFromBackingStore can be costly especially when 104 // it is necessary to read back the web contents image data from GPU. As the 105 // cost is roughly propotional to the number of the copied pixels, the size of 106 // the copied pixels should be as small as possible. 107 // 108 // When the scale factor of the primary monitor is ui::SCALE_FACTOR_100P, 109 // we still copy the pixels with the same size as ui::SCALE_FACTOR_200P because 110 // the resampling method used in RenderWidgetHost::CopyFromBackingStore is not 111 // good enough for the resampled image to be used directly for the thumbnail 112 // (http://crbug.com/141235). We assume this is not an issue in case of 113 // ui::SCALE_FACTOR_200P because the high resolution thumbnail on high density 114 // display alleviates the aliasing. 115 // TODO(mazda): Copy the pixels with the smaller size in the case of 116 // ui::SCALE_FACTOR_100P once the resampling method has been improved. 117 // static 118 gfx::Size SimpleThumbnailCrop::GetCopySizeForThumbnail( 119 ui::ScaleFactor scale_factor, 120 const gfx::Size& thumbnail_size) { 121 gfx::Size copy_size(thumbnail_size); 122 switch (scale_factor) { 123 case ui::SCALE_FACTOR_100P: 124 copy_size = gfx::ToFlooredSize(gfx::ScaleSize( 125 copy_size, ui::GetImageScale(ui::SCALE_FACTOR_200P))); 126 break; 127 case ui::SCALE_FACTOR_200P: 128 // Use the size as-is. 129 break; 130 default: 131 DLOG(WARNING) << "Unsupported scale factor. Use the same copy size as " 132 << "ui::SCALE_FACTOR_100P"; 133 copy_size = gfx::ToFlooredSize(gfx::ScaleSize( 134 copy_size, gfx::ImageSkia::GetMaxSupportedScale())); 135 break; 136 } 137 return copy_size; 138 } 139 140 gfx::Rect SimpleThumbnailCrop::GetClippingRect(const gfx::Size& source_size, 141 const gfx::Size& desired_size, 142 ClipResult* clip_result) { 143 DCHECK(clip_result); 144 145 float desired_aspect = 146 static_cast<float>(desired_size.width()) / desired_size.height(); 147 148 // Get the clipping rect so that we can preserve the aspect ratio while 149 // filling the destination. 150 gfx::Rect clipping_rect; 151 if (source_size.width() < desired_size.width() || 152 source_size.height() < desired_size.height()) { 153 // Source image is smaller: we clip the part of source image within the 154 // dest rect, and then stretch it to fill the dest rect. We don't respect 155 // the aspect ratio in this case. 156 clipping_rect = gfx::Rect(desired_size); 157 *clip_result = thumbnails::CLIP_RESULT_SOURCE_IS_SMALLER; 158 } else { 159 float src_aspect = 160 static_cast<float>(source_size.width()) / source_size.height(); 161 if (src_aspect > desired_aspect) { 162 // Wider than tall, clip horizontally: we center the smaller 163 // thumbnail in the wider screen. 164 int new_width = static_cast<int>(source_size.height() * desired_aspect); 165 int x_offset = (source_size.width() - new_width) / 2; 166 clipping_rect.SetRect(x_offset, 0, new_width, source_size.height()); 167 *clip_result = (src_aspect >= ThumbnailScore::kTooWideAspectRatio) ? 168 thumbnails::CLIP_RESULT_MUCH_WIDER_THAN_TALL : 169 thumbnails::CLIP_RESULT_WIDER_THAN_TALL; 170 } else if (src_aspect < desired_aspect) { 171 clipping_rect = 172 gfx::Rect(source_size.width(), source_size.width() / desired_aspect); 173 *clip_result = thumbnails::CLIP_RESULT_TALLER_THAN_WIDE; 174 } else { 175 clipping_rect = gfx::Rect(source_size); 176 *clip_result = thumbnails::CLIP_RESULT_NOT_CLIPPED; 177 } 178 } 179 return clipping_rect; 180 } 181 182 // static 183 gfx::Size SimpleThumbnailCrop::ComputeTargetSizeAtMaximumScale( 184 const gfx::Size& given_size) { 185 // TODO(mazda|oshima): Update thumbnail when the max scale factor changes. 186 // crbug.com/159157. 187 float max_scale_factor = gfx::ImageSkia::GetMaxSupportedScale(); 188 return gfx::ToFlooredSize(gfx::ScaleSize(given_size, max_scale_factor)); 189 } 190 191 SimpleThumbnailCrop::~SimpleThumbnailCrop() { 192 } 193 194 // Creates a downsampled thumbnail from the given bitmap. 195 // store. The returned bitmap will be isNull if there was an error creating it. 196 SkBitmap SimpleThumbnailCrop::CreateThumbnail(const SkBitmap& bitmap, 197 const gfx::Size& desired_size, 198 ClipResult* clip_result) { 199 base::TimeTicks begin_compute_thumbnail = base::TimeTicks::Now(); 200 201 SkBitmap clipped_bitmap; 202 if (*clip_result == thumbnails::CLIP_RESULT_UNPROCESSED) { 203 // Clip the pixels that will commonly hold a scrollbar, which looks bad in 204 // thumbnails. 205 int scrollbar_size = gfx::scrollbar_size(); 206 SkIRect scrollbarless_rect = 207 { 0, 0, 208 std::max(1, bitmap.width() - scrollbar_size), 209 std::max(1, bitmap.height() - scrollbar_size) }; 210 SkBitmap bmp; 211 bitmap.extractSubset(&bmp, scrollbarless_rect); 212 213 clipped_bitmap = GetClippedBitmap( 214 bmp, desired_size.width(), desired_size.height(), clip_result); 215 } else { 216 clipped_bitmap = bitmap; 217 } 218 219 // Need to resize it to the size we want, so downsample until it's 220 // close, and let the caller make it the exact size if desired. 221 SkBitmap result = SkBitmapOperations::DownsampleByTwoUntilSize( 222 clipped_bitmap, desired_size.width(), desired_size.height()); 223 #if !defined(USE_AURA) 224 // This is a bit subtle. SkBitmaps are refcounted, but the magic 225 // ones in PlatformCanvas can't be assigned to SkBitmap with proper 226 // refcounting. If the bitmap doesn't change, then the downsampler 227 // will return the input bitmap, which will be the reference to the 228 // weird PlatformCanvas one insetad of a regular one. To get a 229 // regular refcounted bitmap, we need to copy it. 230 // 231 // On Aura, the PlatformCanvas is platform-independent and does not have 232 // any native platform resources that can't be refounted, so this issue does 233 // not occur. 234 // 235 // Note that GetClippedBitmap() does extractSubset() but it won't copy 236 // the pixels, hence we check result size == clipped_bitmap size here. 237 if (clipped_bitmap.width() == result.width() && 238 clipped_bitmap.height() == result.height()) 239 clipped_bitmap.copyTo(&result, SkBitmap::kARGB_8888_Config); 240 #endif 241 242 HISTOGRAM_TIMES(kThumbnailHistogramName, 243 base::TimeTicks::Now() - begin_compute_thumbnail); 244 return result; 245 } 246 247 } // namespace thumbnails 248