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, ¤t_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