• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2011 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/tab_contents/thumbnail_generator.h"
6 
7 #include <algorithm>
8 #include <map>
9 
10 #include "base/memory/scoped_ptr.h"
11 #include "base/metrics/histogram.h"
12 #include "base/time.h"
13 #include "build/build_config.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/history/top_sites.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/common/thumbnail_score.h"
18 #include "content/browser/renderer_host/backing_store.h"
19 #include "content/browser/renderer_host/render_process_host.h"
20 #include "content/browser/renderer_host/render_view_host.h"
21 #include "content/browser/tab_contents/tab_contents.h"
22 #include "content/common/notification_service.h"
23 #include "content/common/property_bag.h"
24 #include "googleurl/src/gurl.h"
25 #include "skia/ext/bitmap_platform_device.h"
26 #include "skia/ext/image_operations.h"
27 #include "skia/ext/platform_canvas.h"
28 #include "third_party/skia/include/core/SkBitmap.h"
29 #include "ui/gfx/color_utils.h"
30 #include "ui/gfx/rect.h"
31 #include "ui/gfx/skbitmap_operations.h"
32 
33 #if defined(OS_WIN)
34 #include "content/common/section_util_win.h"
35 #endif
36 
37 // Overview
38 // --------
39 // This class provides current thumbnails for tabs. The simplest operation is
40 // when a request for a thumbnail comes in, to grab the backing store and make
41 // a smaller version of that. Clients of the class can send such a request by
42 // GetThumbnailForRenderer() and AskForSnapshot().
43 //
44 // The class also provides a service for updating thumbnails to be used in
45 // "Most visited" section of the new tab page. The service can be started
46 // by StartThumbnailing(). The current algorithm of the service is as
47 // simple as follows:
48 //
49 //    When a renderer is about to be hidden (this usually occurs when the
50 //    current tab is closed or another tab is clicked), update the
51 //    thumbnail for the tab rendered by the renderer, if needed. The
52 //    heuristics to judge whether or not to update the thumbnail is
53 //    implemented in ShouldUpdateThumbnail().
54 //
55 // We'll likely revise the algorithm to improve quality of thumbnails this
56 // service generates.
57 
58 namespace {
59 
60 static const int kThumbnailWidth = 212;
61 static const int kThumbnailHeight = 132;
62 
63 static const char kThumbnailHistogramName[] = "Thumbnail.ComputeMS";
64 
65 // Returns a property accessor used for attaching a TabContents to a
66 // RenderWidgetHost. We maintain the RenderWidgetHost to TabContents
67 // mapping so that we can retrieve a TabContents from a RenderWidgetHost
GetTabContentsAccessor()68 PropertyAccessor<TabContents*>* GetTabContentsAccessor() {
69   static PropertyAccessor<TabContents*> accessor;
70   return &accessor;
71 }
72 
73 // Creates a downsampled thumbnail for the given backing store. The returned
74 // bitmap will be isNull if there was an error creating it.
GetBitmapForBackingStore(BackingStore * backing_store,int desired_width,int desired_height,int options,ThumbnailGenerator::ClipResult * clip_result)75 SkBitmap GetBitmapForBackingStore(
76     BackingStore* backing_store,
77     int desired_width,
78     int desired_height,
79     int options,
80     ThumbnailGenerator::ClipResult* clip_result) {
81   base::TimeTicks begin_compute_thumbnail = base::TimeTicks::Now();
82 
83   SkBitmap result;
84 
85   // Get the bitmap as a Skia object so we can resample it. This is a large
86   // allocation and we can tolerate failure here, so give up if the allocation
87   // fails.
88   skia::PlatformCanvas temp_canvas;
89   if (!backing_store->CopyFromBackingStore(gfx::Rect(backing_store->size()),
90                                            &temp_canvas))
91     return result;
92   const SkBitmap& bmp = temp_canvas.getTopPlatformDevice().accessBitmap(false);
93 
94   // Check if a clipped thumbnail is requested.
95   if (options & ThumbnailGenerator::kClippedThumbnail) {
96     SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap(
97         bmp, desired_width, desired_height, clip_result);
98 
99     // Need to resize it to the size we want, so downsample until it's
100     // close, and let the caller make it the exact size if desired.
101     result = SkBitmapOperations::DownsampleByTwoUntilSize(
102         clipped_bitmap, desired_width, desired_height);
103   } else {
104     // Need to resize it to the size we want, so downsample until it's
105     // close, and let the caller make it the exact size if desired.
106     result = SkBitmapOperations::DownsampleByTwoUntilSize(
107         bmp, desired_width, desired_height);
108 
109     // This is a bit subtle. SkBitmaps are refcounted, but the magic
110     // ones in PlatformCanvas can't be assigned to SkBitmap with proper
111     // refcounting.  If the bitmap doesn't change, then the downsampler
112     // will return the input bitmap, which will be the reference to the
113     // weird PlatformCanvas one insetad of a regular one. To get a
114     // regular refcounted bitmap, we need to copy it.
115     if (bmp.width() == result.width() &&
116         bmp.height() == result.height())
117       bmp.copyTo(&result, SkBitmap::kARGB_8888_Config);
118   }
119 
120   HISTOGRAM_TIMES(kThumbnailHistogramName,
121                   base::TimeTicks::Now() - begin_compute_thumbnail);
122   return result;
123 }
124 
125 }  // namespace
126 
127 struct ThumbnailGenerator::AsyncRequestInfo {
128   scoped_ptr<ThumbnailReadyCallback> callback;
129   scoped_ptr<TransportDIB> thumbnail_dib;
130   RenderWidgetHost* renderer;  // Not owned.
131 };
132 
ThumbnailGenerator()133 ThumbnailGenerator::ThumbnailGenerator() {
134   // The BrowserProcessImpl creates this non-lazily. If you add nontrivial
135   // stuff here, be sure to convert it to being lazily created.
136   //
137   // We don't register for notifications here since BrowserProcessImpl creates
138   // us before the NotificationService is.
139 }
140 
~ThumbnailGenerator()141 ThumbnailGenerator::~ThumbnailGenerator() {
142 }
143 
StartThumbnailing()144 void ThumbnailGenerator::StartThumbnailing() {
145   if (registrar_.IsEmpty()) {
146     // Even though we deal in RenderWidgetHosts, we only care about its
147     // subclass, RenderViewHost when it is in a tab. We don't make thumbnails
148     // for RenderViewHosts that aren't in tabs, or RenderWidgetHosts that
149     // aren't views like select popups.
150     registrar_.Add(this, NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB,
151                    NotificationService::AllSources());
152     registrar_.Add(this, NotificationType::RENDER_WIDGET_VISIBILITY_CHANGED,
153                    NotificationService::AllSources());
154     registrar_.Add(this, NotificationType::TAB_CONTENTS_DISCONNECTED,
155                    NotificationService::AllSources());
156   }
157 }
158 
MonitorRenderer(RenderWidgetHost * renderer,bool monitor)159 void ThumbnailGenerator::MonitorRenderer(RenderWidgetHost* renderer,
160                                          bool monitor) {
161   Source<RenderWidgetHost> renderer_source = Source<RenderWidgetHost>(renderer);
162   bool currently_monitored =
163       registrar_.IsRegistered(
164         this,
165         NotificationType::RENDER_WIDGET_HOST_DID_RECEIVE_PAINT_AT_SIZE_ACK,
166         renderer_source);
167   if (monitor != currently_monitored) {
168     if (monitor) {
169       registrar_.Add(
170           this,
171           NotificationType::RENDER_WIDGET_HOST_DID_RECEIVE_PAINT_AT_SIZE_ACK,
172           renderer_source);
173     } else {
174       registrar_.Remove(
175           this,
176           NotificationType::RENDER_WIDGET_HOST_DID_RECEIVE_PAINT_AT_SIZE_ACK,
177           renderer_source);
178     }
179   }
180 }
181 
AskForSnapshot(RenderWidgetHost * renderer,bool prefer_backing_store,ThumbnailReadyCallback * callback,gfx::Size page_size,gfx::Size desired_size)182 void ThumbnailGenerator::AskForSnapshot(RenderWidgetHost* renderer,
183                                         bool prefer_backing_store,
184                                         ThumbnailReadyCallback* callback,
185                                         gfx::Size page_size,
186                                         gfx::Size desired_size) {
187   if (prefer_backing_store) {
188     BackingStore* backing_store = renderer->GetBackingStore(false);
189     if (backing_store) {
190       // We were able to find a non-null backing store for this renderer, so
191       // we'll go with it.
192       SkBitmap first_try = GetBitmapForBackingStore(backing_store,
193                                                     desired_size.width(),
194                                                     desired_size.height(),
195                                                     kNoOptions,
196                                                     NULL);
197       callback->Run(first_try);
198 
199       delete callback;
200       return;
201     }
202     // Now, if the backing store didn't exist, we will still try and
203     // render asynchronously.
204   }
205 
206   // We are going to render the thumbnail asynchronously now, so keep
207   // this callback for later lookup when the rendering is done.
208   static int sequence_num = 0;
209   sequence_num++;
210   scoped_ptr<TransportDIB> thumbnail_dib(TransportDIB::Create(
211       desired_size.width() * desired_size.height() * 4, sequence_num));
212 
213 #if defined(OS_WIN)
214   // Duplicate the handle to the DIB here because the renderer process does not
215   // have permission. The duplicated handle is owned by the renderer process,
216   // which is responsible for closing it.
217   TransportDIB::Handle renderer_dib_handle = chrome::GetSectionForProcess(
218       thumbnail_dib->handle(),
219       renderer->process()->GetHandle(),
220       false);
221   if (!renderer_dib_handle) {
222     LOG(WARNING) << "Could not duplicate dib handle for renderer";
223     delete callback;
224     return;
225   }
226 #else
227   TransportDIB::Handle renderer_dib_handle = thumbnail_dib->handle();
228 #endif
229 
230   linked_ptr<AsyncRequestInfo> request_info(new AsyncRequestInfo);
231   request_info->callback.reset(callback);
232   request_info->thumbnail_dib.reset(thumbnail_dib.release());
233   request_info->renderer = renderer;
234   ThumbnailCallbackMap::value_type new_value(sequence_num, request_info);
235   std::pair<ThumbnailCallbackMap::iterator, bool> result =
236       callback_map_.insert(new_value);
237   if (!result.second) {
238     NOTREACHED() << "Callback already registered?";
239     return;
240   }
241 
242   renderer->PaintAtSize(
243       renderer_dib_handle, sequence_num, page_size, desired_size);
244 }
245 
GetThumbnailForRenderer(RenderWidgetHost * renderer) const246 SkBitmap ThumbnailGenerator::GetThumbnailForRenderer(
247     RenderWidgetHost* renderer) const {
248   return GetThumbnailForRendererWithOptions(renderer, kNoOptions, NULL);
249 }
250 
GetThumbnailForRendererWithOptions(RenderWidgetHost * renderer,int options,ClipResult * clip_result) const251 SkBitmap ThumbnailGenerator::GetThumbnailForRendererWithOptions(
252     RenderWidgetHost* renderer,
253     int options,
254     ClipResult* clip_result) const {
255   BackingStore* backing_store = renderer->GetBackingStore(false);
256   if (!backing_store) {
257     // When we have no backing store, there's no choice in what to use. We
258     // have to return the empty thumbnail.
259     return SkBitmap();
260   }
261 
262   return GetBitmapForBackingStore(backing_store,
263                                   kThumbnailWidth,
264                                   kThumbnailHeight,
265                                   options,
266                                   clip_result);
267 }
268 
WidgetDidReceivePaintAtSizeAck(RenderWidgetHost * widget,int sequence_num,const gfx::Size & size)269 void ThumbnailGenerator::WidgetDidReceivePaintAtSizeAck(
270     RenderWidgetHost* widget,
271     int sequence_num,
272     const gfx::Size& size) {
273   // Lookup the callback, run it, and erase it.
274   ThumbnailCallbackMap::iterator item = callback_map_.find(sequence_num);
275   if (item != callback_map_.end()) {
276     TransportDIB* dib = item->second->thumbnail_dib.get();
277     DCHECK(dib);
278     if (!dib || !dib->Map()) {
279       return;
280     }
281 
282     // Create an SkBitmap from the DIB.
283     SkBitmap non_owned_bitmap;
284     SkBitmap result;
285 
286     // Fill out the non_owned_bitmap with the right config.  Note that
287     // this code assumes that the transport dib is a 32-bit ARGB
288     // image.
289     non_owned_bitmap.setConfig(SkBitmap::kARGB_8888_Config,
290                                size.width(), size.height());
291     non_owned_bitmap.setPixels(dib->memory());
292 
293     // Now alloc/copy the memory so we own it and can pass it around,
294     // and the memory won't go away when the DIB goes away.
295     // TODO: Figure out a way to avoid this copy?
296     non_owned_bitmap.copyTo(&result, SkBitmap::kARGB_8888_Config);
297 
298     item->second->callback->Run(result);
299 
300     // We're done with the callback, and with the DIB, so delete both.
301     callback_map_.erase(item);
302   }
303 }
304 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)305 void ThumbnailGenerator::Observe(NotificationType type,
306                                  const NotificationSource& source,
307                                  const NotificationDetails& details) {
308   switch (type.value) {
309     case NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB: {
310       // Install our observer for all new RVHs.
311       RenderViewHost* renderer = Details<RenderViewHost>(details).ptr();
312       TabContents* contents = Source<TabContents>(source).ptr();
313       MonitorRenderer(renderer, true);
314       // Attach the tab contents to the renderer.
315       // TODO(satorux): Rework this code. This relies on some internals of
316       // how TabContents and RVH work. We should make this class
317       // per-tab. See also crbug.com/78990.
318       GetTabContentsAccessor()->SetProperty(
319           renderer->property_bag(), contents);
320       VLOG(1) << "renderer " << renderer << "is created for tab " << contents;
321       break;
322     }
323 
324     case NotificationType::RENDER_WIDGET_VISIBILITY_CHANGED:
325       if (!*Details<bool>(details).ptr())
326         WidgetHidden(Source<RenderWidgetHost>(source).ptr());
327       break;
328 
329     case NotificationType::RENDER_WIDGET_HOST_DID_RECEIVE_PAINT_AT_SIZE_ACK: {
330       RenderWidgetHost::PaintAtSizeAckDetails* size_ack_details =
331           Details<RenderWidgetHost::PaintAtSizeAckDetails>(details).ptr();
332       WidgetDidReceivePaintAtSizeAck(
333           Source<RenderWidgetHost>(source).ptr(),
334           size_ack_details->tag,
335           size_ack_details->size);
336       break;
337     }
338 
339     case NotificationType::TAB_CONTENTS_DISCONNECTED:
340       TabContentsDisconnected(Source<TabContents>(source).ptr());
341       break;
342 
343     default:
344       NOTREACHED() << "Unexpected notification type: " << type.value;
345   }
346 }
347 
WidgetHidden(RenderWidgetHost * widget)348 void ThumbnailGenerator::WidgetHidden(RenderWidgetHost* widget) {
349   // Retrieve the tab contents rendered by the widget.
350   TabContents** property = GetTabContentsAccessor()->GetProperty(
351       widget->property_bag());
352   if (!property) {
353     LOG(ERROR) << "This widget is not associated with tab contents: "
354                << widget;
355     return;
356   }
357   TabContents* contents = *property;
358   UpdateThumbnailIfNecessary(contents);
359 }
360 
TabContentsDisconnected(TabContents * contents)361 void ThumbnailGenerator::TabContentsDisconnected(TabContents* contents) {
362   // Go through the existing callbacks, and find any that have the
363   // same renderer as this TabContents and remove them so they don't
364   // hang around.
365   ThumbnailCallbackMap::iterator iterator = callback_map_.begin();
366   RenderWidgetHost* renderer = contents->render_view_host();
367   while (iterator != callback_map_.end()) {
368     if (iterator->second->renderer == renderer) {
369       ThumbnailCallbackMap::iterator nuked = iterator;
370       ++iterator;
371       callback_map_.erase(nuked);
372       continue;
373     }
374     ++iterator;
375   }
376 }
377 
CalculateBoringScore(SkBitmap * bitmap)378 double ThumbnailGenerator::CalculateBoringScore(SkBitmap* bitmap) {
379   if (bitmap->isNull() || bitmap->empty())
380     return 1.0;
381   int histogram[256] = {0};
382   color_utils::BuildLumaHistogram(bitmap, histogram);
383 
384   int color_count = *std::max_element(histogram, histogram + 256);
385   int pixel_count = bitmap->width() * bitmap->height();
386   return static_cast<double>(color_count) / pixel_count;
387 }
388 
GetClippedBitmap(const SkBitmap & bitmap,int desired_width,int desired_height,ClipResult * clip_result)389 SkBitmap ThumbnailGenerator::GetClippedBitmap(const SkBitmap& bitmap,
390                                               int desired_width,
391                                               int desired_height,
392                                               ClipResult* clip_result) {
393   const SkRect dest_rect = { 0, 0,
394                              SkIntToScalar(desired_width),
395                              SkIntToScalar(desired_height) };
396   const float dest_aspect = dest_rect.width() / dest_rect.height();
397 
398   // Get the src rect so that we can preserve the aspect ratio while filling
399   // the destination.
400   SkIRect src_rect;
401   if (bitmap.width() < dest_rect.width() ||
402       bitmap.height() < dest_rect.height()) {
403     // Source image is smaller: we clip the part of source image within the
404     // dest rect, and then stretch it to fill the dest rect. We don't respect
405     // the aspect ratio in this case.
406     src_rect.set(0, 0, static_cast<S16CPU>(dest_rect.width()),
407                  static_cast<S16CPU>(dest_rect.height()));
408     if (clip_result)
409       *clip_result = ThumbnailGenerator::kSourceIsSmaller;
410   } else {
411     const float src_aspect =
412         static_cast<float>(bitmap.width()) / bitmap.height();
413     if (src_aspect > dest_aspect) {
414       // Wider than tall, clip horizontally: we center the smaller
415       // thumbnail in the wider screen.
416       S16CPU new_width = static_cast<S16CPU>(bitmap.height() * dest_aspect);
417       S16CPU x_offset = (bitmap.width() - new_width) / 2;
418       src_rect.set(x_offset, 0, new_width + x_offset, bitmap.height());
419       if (clip_result)
420         *clip_result = ThumbnailGenerator::kWiderThanTall;
421     } else if (src_aspect < dest_aspect) {
422       src_rect.set(0, 0, bitmap.width(),
423                    static_cast<S16CPU>(bitmap.width() / dest_aspect));
424       if (clip_result)
425         *clip_result = ThumbnailGenerator::kTallerThanWide;
426     } else {
427       src_rect.set(0, 0, bitmap.width(), bitmap.height());
428       if (clip_result)
429         *clip_result = ThumbnailGenerator::kNotClipped;
430     }
431   }
432 
433   SkBitmap clipped_bitmap;
434   bitmap.extractSubset(&clipped_bitmap, src_rect);
435   return clipped_bitmap;
436 }
437 
UpdateThumbnailIfNecessary(TabContents * tab_contents)438 void ThumbnailGenerator::UpdateThumbnailIfNecessary(
439     TabContents* tab_contents) {
440   const GURL& url = tab_contents->GetURL();
441   history::TopSites* top_sites = tab_contents->profile()->GetTopSites();
442   // Skip if we don't need to update the thumbnail.
443   if (!ShouldUpdateThumbnail(tab_contents->profile(), top_sites, url))
444     return;
445 
446   const int options = ThumbnailGenerator::kClippedThumbnail;
447   ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped;
448   SkBitmap thumbnail = GetThumbnailForRendererWithOptions(
449       tab_contents->render_view_host(), options, &clip_result);
450   // Failed to generate a thumbnail. Maybe the tab is in the background?
451   if (thumbnail.isNull())
452     return;
453 
454   // Compute the thumbnail score.
455   ThumbnailScore score;
456   score.at_top =
457       (tab_contents->render_view_host()->last_scroll_offset().y() == 0);
458   score.boring_score = ThumbnailGenerator::CalculateBoringScore(&thumbnail);
459   score.good_clipping =
460       (clip_result == ThumbnailGenerator::kTallerThanWide ||
461        clip_result == ThumbnailGenerator::kNotClipped);
462 
463   top_sites->SetPageThumbnail(url, thumbnail, score);
464   VLOG(1) << "Thumbnail taken for " << url << ": " << score.ToString();
465 }
466 
ShouldUpdateThumbnail(Profile * profile,history::TopSites * top_sites,const GURL & url)467 bool ThumbnailGenerator::ShouldUpdateThumbnail(Profile* profile,
468                                                history::TopSites* top_sites,
469                                                const GURL& url) {
470   if (!profile || !top_sites)
471     return false;
472   // Skip if it's in the incognito mode.
473   if (profile->IsOffTheRecord())
474     return false;
475   // Skip if the given URL is not appropriate for history.
476   if (!HistoryService::CanAddURL(url))
477     return false;
478   // Skip if the top sites list is full, and the URL is not known.
479   if (top_sites->IsFull() && !top_sites->IsKnownURL(url))
480     return false;
481   // Skip if we don't have to udpate the existing thumbnail.
482   ThumbnailScore current_score;
483   if (top_sites->GetPageThumbnailScore(url, &current_score) &&
484       !current_score.ShouldConsiderUpdating())
485     return false;
486   // Skip if we don't have to udpate the temporary thumbnail (i.e. the one
487   // not yet saved).
488   ThumbnailScore temporary_score;
489   if (top_sites->GetTemporaryPageThumbnailScore(url, &temporary_score) &&
490       !temporary_score.ShouldConsiderUpdating())
491     return false;
492 
493   return true;
494 }
495