// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/thumbnails/thumbnail_tab_helper.h" #include "base/metrics/histogram.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/thumbnails/render_widget_snapshot_taker.h" #include "chrome/browser/thumbnails/thumbnail_service.h" #include "chrome/browser/thumbnails/thumbnail_service_factory.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host_view.h" #include "skia/ext/platform_canvas.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/size_conversions.h" #include "ui/gfx/screen.h" #include "ui/gfx/scrollbar_size.h" #include "ui/gfx/skbitmap_operations.h" #if defined(OS_WIN) #include "base/win/windows_version.h" #endif DEFINE_WEB_CONTENTS_USER_DATA_KEY(ThumbnailTabHelper) // Overview // -------- // This class provides a service for updating thumbnails to be used in // "Most visited" section of the new tab page. The service can be started // by StartThumbnailing(). The current algorithm of the service is as // simple as follows: // // When a renderer is about to be hidden (this usually occurs when the // current tab is closed or another tab is clicked), update the // thumbnail for the tab rendered by the renderer, if needed. The // heuristics to judge whether or not to update the thumbnail is // implemented in ShouldUpdateThumbnail(). using content::RenderViewHost; using content::RenderWidgetHost; using content::WebContents; namespace { // The thumbnail size in DIP. static const int kThumbnailWidth = 212; static const int kThumbnailHeight = 132; static const char kThumbnailHistogramName[] = "Thumbnail.ComputeMS"; // Returns the size used by RenderWidgetHost::CopyFromBackingStore. // // The size is calculated in such a way that the copied size in pixel becomes // equal to (f * kThumbnailWidth, f * kThumbnailHeight), where f is the scale // of ui::SCALE_FACTOR_200P. Since RenderWidgetHost::CopyFromBackingStore takes // the size in DIP, we need to adjust the size based on |view|'s device scale // factor in order to copy the pixels with the size above. // // The copied size was chosen for the following reasons. // // 1. When the scale factor of the primary monitor is ui::SCALE_FACTOR_200P, the // generated thumbnail size is (f * kThumbnailWidth, f * kThumbnailHeight). // In order to avoid degrading the image quality by magnification, the size // of the copied pixels should be equal to or larger than this thumbnail size. // // 2. RenderWidgetHost::CopyFromBackingStore can be costly especially when // it is necessary to read back the web contents image data from GPU. As the // cost is roughly propotional to the number of the copied pixels, the size of // the copied pixels should be as small as possible. // // When the scale factor of the primary monitor is ui::SCALE_FACTOR_100P, // we still copy the pixels with the same size as ui::SCALE_FACTOR_200P because // the resampling method used in RenderWidgetHost::CopyFromBackingStore is not // good enough for the resampled image to be used directly for the thumbnail // (http://crbug.com/141235). We assume this is not an issue in case of // ui::SCALE_FACTOR_200P because the high resolution thumbnail on high density // display alleviates the aliasing. // TODO(mazda): Copy the pixels with the smaller size in the case of // ui::SCALE_FACTOR_100P once the resampling method has been improved. gfx::Size GetCopySizeForThumbnail(content::RenderWidgetHostView* view) { gfx::Size copy_size(kThumbnailWidth, kThumbnailHeight); ui::ScaleFactor scale_factor = ui::GetScaleFactorForNativeView(view->GetNativeView()); switch (scale_factor) { case ui::SCALE_FACTOR_100P: copy_size = gfx::ToFlooredSize(gfx::ScaleSize( copy_size, ui::GetScaleFactorScale(ui::SCALE_FACTOR_200P))); break; case ui::SCALE_FACTOR_200P: // Use the size as-is. break; default: DLOG(WARNING) << "Unsupported scale factor. Use the same copy size as " << "ui::SCALE_FACTOR_100P"; copy_size = gfx::ToFlooredSize(gfx::ScaleSize( copy_size, ui::GetScaleFactorScale(ui::SCALE_FACTOR_200P))); break; } return copy_size; } // Returns the size of the thumbnail stored in the database in pixel. gfx::Size GetThumbnailSizeInPixel() { gfx::Size thumbnail_size(kThumbnailWidth, kThumbnailHeight); // Determine the resolution of the thumbnail based on the maximum scale // factor. // TODO(mazda|oshima): Update thumbnail when the max scale factor changes. // crbug.com/159157. float max_scale_factor = ui::GetScaleFactorScale(ui::GetMaxScaleFactor()); return gfx::ToFlooredSize(gfx::ScaleSize(thumbnail_size, max_scale_factor)); } // Returns the clipping rectangle that is used for creating a thumbnail with // the size of |desired_size| from the bitmap with the size of |source_size|. // The type of clipping that needs to be done is assigned to |clip_result|. gfx::Rect GetClippingRect(const gfx::Size& source_size, const gfx::Size& desired_size, ThumbnailTabHelper::ClipResult* clip_result) { DCHECK(clip_result); float desired_aspect = static_cast(desired_size.width()) / desired_size.height(); // Get the clipping rect so that we can preserve the aspect ratio while // filling the destination. gfx::Rect clipping_rect; if (source_size.width() < desired_size.width() || source_size.height() < desired_size.height()) { // Source image is smaller: we clip the part of source image within the // dest rect, and then stretch it to fill the dest rect. We don't respect // the aspect ratio in this case. clipping_rect = gfx::Rect(desired_size); *clip_result = ThumbnailTabHelper::kSourceIsSmaller; } else { float src_aspect = static_cast(source_size.width()) / source_size.height(); if (src_aspect > desired_aspect) { // Wider than tall, clip horizontally: we center the smaller // thumbnail in the wider screen. int new_width = static_cast(source_size.height() * desired_aspect); int x_offset = (source_size.width() - new_width) / 2; clipping_rect.SetRect(x_offset, 0, new_width, source_size.height()); *clip_result = (src_aspect >= ThumbnailScore::kTooWideAspectRatio) ? ThumbnailTabHelper::kTooWiderThanTall : ThumbnailTabHelper::kWiderThanTall; } else if (src_aspect < desired_aspect) { clipping_rect = gfx::Rect(source_size.width(), source_size.width() / desired_aspect); *clip_result = ThumbnailTabHelper::kTallerThanWide; } else { clipping_rect = gfx::Rect(source_size); *clip_result = ThumbnailTabHelper::kNotClipped; } } return clipping_rect; } // Creates a downsampled thumbnail from the given bitmap. // store. The returned bitmap will be isNull if there was an error creating it. SkBitmap CreateThumbnail( const SkBitmap& bitmap, const gfx::Size& desired_size, ThumbnailTabHelper::ClipResult* clip_result) { base::TimeTicks begin_compute_thumbnail = base::TimeTicks::Now(); SkBitmap clipped_bitmap; if (*clip_result == ThumbnailTabHelper::kUnprocessed) { // Clip the pixels that will commonly hold a scrollbar, which looks bad in // thumbnails. int scrollbar_size = gfx::scrollbar_size(); SkIRect scrollbarless_rect = { 0, 0, std::max(1, bitmap.width() - scrollbar_size), std::max(1, bitmap.height() - scrollbar_size) }; SkBitmap bmp; bitmap.extractSubset(&bmp, scrollbarless_rect); clipped_bitmap = ThumbnailTabHelper::GetClippedBitmap( bmp, desired_size.width(), desired_size.height(), clip_result); } else { clipped_bitmap = bitmap; } // Need to resize it to the size we want, so downsample until it's // close, and let the caller make it the exact size if desired. SkBitmap result = SkBitmapOperations::DownsampleByTwoUntilSize( clipped_bitmap, desired_size.width(), desired_size.height()); #if !defined(USE_AURA) // This is a bit subtle. SkBitmaps are refcounted, but the magic // ones in PlatformCanvas can't be assigned to SkBitmap with proper // refcounting. If the bitmap doesn't change, then the downsampler // will return the input bitmap, which will be the reference to the // weird PlatformCanvas one insetad of a regular one. To get a // regular refcounted bitmap, we need to copy it. // // On Aura, the PlatformCanvas is platform-independent and does not have // any native platform resources that can't be refounted, so this issue does // not occur. // // Note that GetClippedBitmap() does extractSubset() but it won't copy // the pixels, hence we check result size == clipped_bitmap size here. if (clipped_bitmap.width() == result.width() && clipped_bitmap.height() == result.height()) clipped_bitmap.copyTo(&result, SkBitmap::kARGB_8888_Config); #endif HISTOGRAM_TIMES(kThumbnailHistogramName, base::TimeTicks::Now() - begin_compute_thumbnail); return result; } } // namespace ThumbnailTabHelper::ThumbnailingContext::ThumbnailingContext( content::WebContents* web_contents, bool load_interrupted) : browser_context(web_contents->GetBrowserContext()), url(web_contents->GetURL()), clip_result(kUnprocessed) { score.at_top = (web_contents->GetRenderViewHost()->GetLastScrollOffset().y() == 0); score.load_completed = !web_contents->IsLoading() && !load_interrupted; } ThumbnailTabHelper::ThumbnailingContext::~ThumbnailingContext() { } ThumbnailTabHelper::ThumbnailTabHelper(content::WebContents* contents) : content::WebContentsObserver(contents), enabled_(true), load_interrupted_(false) { // Even though we deal in RenderWidgetHosts, we only care about its // subclass, RenderViewHost when it is in a tab. We don't make thumbnails // for RenderViewHosts that aren't in tabs, or RenderWidgetHosts that // aren't views like select popups. registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED, content::Source(contents)); } ThumbnailTabHelper::~ThumbnailTabHelper() { } void ThumbnailTabHelper::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED: RenderViewHostCreated(content::Details(details).ptr()); break; case content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED: if (!*content::Details(details).ptr()) WidgetHidden(content::Source(source).ptr()); break; case content::NOTIFICATION_RENDER_VIEW_HOST_DELETED: RenderViewHostDeleted(content::Source(source).ptr()); break; default: NOTREACHED() << "Unexpected notification type: " << type; } } void ThumbnailTabHelper::RenderViewHostCreated( content::RenderViewHost* renderer) { // NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED is really a new // RenderView, not RenderViewHost, and there is no good way to get // notifications of RenderViewHosts. So just be tolerant of re-registrations. bool registered = registrar_.IsRegistered( this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED, content::Source(renderer)); if (!registered) { registrar_.Add( this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED, content::Source(renderer)); registrar_.Add( this, content::NOTIFICATION_RENDER_VIEW_HOST_DELETED, content::Source(renderer)); } } void ThumbnailTabHelper::WidgetHidden(RenderWidgetHost* widget) { if (!enabled_) return; UpdateThumbnailIfNecessary(web_contents()); } void ThumbnailTabHelper::RenderViewHostDeleted( content::RenderViewHost* renderer) { g_browser_process->GetRenderWidgetSnapshotTaker()->CancelSnapshot(renderer); registrar_.Remove( this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED, content::Source(renderer)); registrar_.Remove( this, content::NOTIFICATION_RENDER_VIEW_HOST_DELETED, content::Source(renderer)); } double ThumbnailTabHelper::CalculateBoringScore(const SkBitmap& bitmap) { if (bitmap.isNull() || bitmap.empty()) return 1.0; int histogram[256] = {0}; color_utils::BuildLumaHistogram(bitmap, histogram); int color_count = *std::max_element(histogram, histogram + 256); int pixel_count = bitmap.width() * bitmap.height(); return static_cast(color_count) / pixel_count; } SkBitmap ThumbnailTabHelper::GetClippedBitmap(const SkBitmap& bitmap, int desired_width, int desired_height, ClipResult* clip_result) { gfx::Rect clipping_rect = GetClippingRect(gfx::Size(bitmap.width(), bitmap.height()), gfx::Size(desired_width, desired_height), clip_result); SkIRect src_rect = { clipping_rect.x(), clipping_rect.y(), clipping_rect.right(), clipping_rect.bottom() }; SkBitmap clipped_bitmap; bitmap.extractSubset(&clipped_bitmap, src_rect); return clipped_bitmap; } void ThumbnailTabHelper::UpdateThumbnailIfNecessary( WebContents* web_contents) { // Destroying a WebContents may trigger it to be hidden, prompting a snapshot // which would be unwise to attempt . If the // WebContents is in the middle of destruction, do not risk it. if (web_contents->IsBeingDestroyed()) return; // Skip if a pending entry exists. WidgetHidden can be called while navigating // pages and this is not a time when thumbnails should be generated. if (web_contents->GetController().GetPendingEntry()) return; const GURL& url = web_contents->GetURL(); Profile* profile = Profile::FromBrowserContext(web_contents->GetBrowserContext()); scoped_refptr thumbnail_service = ThumbnailServiceFactory::GetForProfile(profile); // Skip if we don't need to update the thumbnail. if (thumbnail_service == NULL || !thumbnail_service->ShouldAcquirePageThumbnail(url)) { return; } AsyncUpdateThumbnail(web_contents); } void ThumbnailTabHelper::UpdateThumbnail( ThumbnailingContext* context, const SkBitmap& thumbnail) { Profile* profile = Profile::FromBrowserContext(context->browser_context); scoped_refptr thumbnail_service = ThumbnailServiceFactory::GetForProfile(profile); if (!thumbnail_service) return; context->score.boring_score = CalculateBoringScore(thumbnail); context->score.good_clipping = (context->clip_result == ThumbnailTabHelper::kWiderThanTall || context->clip_result == ThumbnailTabHelper::kTallerThanWide || context->clip_result == ThumbnailTabHelper::kNotClipped); gfx::Image image(thumbnail); thumbnail_service->SetPageThumbnail(context->url, image, context->score); VLOG(1) << "Thumbnail taken for " << context->url << ": " << context->score.ToString(); } void ThumbnailTabHelper::AsyncUpdateThumbnail( WebContents* web_contents) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); RenderWidgetHost* render_widget_host = web_contents->GetRenderViewHost(); content::RenderWidgetHostView* view = render_widget_host->GetView(); if (!view) return; if (!view->IsSurfaceAvailableForCopy()) { #if defined(OS_WIN) // On Windows XP, neither the backing store nor the compositing surface is // available in the browser when accelerated compositing is active, so ask // the renderer to send a snapshot for creating the thumbnail. if (base::win::GetVersion() < base::win::VERSION_VISTA) { scoped_refptr context( new ThumbnailingContext(web_contents, load_interrupted_)); gfx::Size view_size = render_widget_host->GetView()->GetViewBounds().size(); g_browser_process->GetRenderWidgetSnapshotTaker()->AskForSnapshot( render_widget_host, base::Bind(&ThumbnailTabHelper::UpdateThumbnailWithBitmap, context), view_size, view_size); } #endif return; } gfx::Rect copy_rect = gfx::Rect(view->GetViewBounds().size()); // Clip the pixels that will commonly hold a scrollbar, which looks bad in // thumbnails. int scrollbar_size = gfx::scrollbar_size(); copy_rect.Inset(0, 0, scrollbar_size, scrollbar_size); scoped_refptr context( new ThumbnailingContext(web_contents, load_interrupted_)); copy_rect = GetClippingRect(copy_rect.size(), gfx::Size(kThumbnailWidth, kThumbnailHeight), &context->clip_result); gfx::Size copy_size = GetCopySizeForThumbnail(view); skia::PlatformBitmap* temp_bitmap = new skia::PlatformBitmap; render_widget_host->CopyFromBackingStore( copy_rect, copy_size, base::Bind(&ThumbnailTabHelper::UpdateThumbnailWithCanvas, context, base::Owned(temp_bitmap)), temp_bitmap); } void ThumbnailTabHelper::UpdateThumbnailWithBitmap( ThumbnailingContext* context, const SkBitmap& bitmap) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); if (bitmap.isNull() || bitmap.empty()) return; SkBitmap thumbnail = CreateThumbnail(bitmap, GetThumbnailSizeInPixel(), &context->clip_result); UpdateThumbnail(context, thumbnail); } void ThumbnailTabHelper::UpdateThumbnailWithCanvas( ThumbnailingContext* context, skia::PlatformBitmap* temp_bitmap, bool succeeded) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); if (!succeeded) return; SkBitmap bitmap = temp_bitmap->GetBitmap(); UpdateThumbnailWithBitmap(context, bitmap); } void ThumbnailTabHelper::DidStartLoading( content::RenderViewHost* render_view_host) { load_interrupted_ = false; } void ThumbnailTabHelper::StopNavigation() { // This function gets called when the page loading is interrupted by the // stop button. load_interrupted_ = true; }