OLD | NEW |
| (Empty) |
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/renderer/chrome_render_observer.h" | |
6 | |
7 #include "base/command_line.h" | |
8 #include "base/message_loop.h" | |
9 #include "base/metrics/histogram.h" | |
10 #include "chrome/common/chrome_switches.h" | |
11 #include "chrome/common/render_messages.h" | |
12 #include "chrome/common/thumbnail_score.h" | |
13 #include "chrome/renderer/safe_browsing/phishing_classifier_delegate.h" | |
14 #include "chrome/renderer/translate_helper.h" | |
15 #include "content/renderer/content_renderer_client.h" | |
16 #include "content/renderer/render_view.h" | |
17 #include "skia/ext/bitmap_platform_device.h" | |
18 #include "skia/ext/image_operations.h" | |
19 #include "skia/ext/platform_canvas.h" | |
20 #include "third_party/WebKit/Source/WebKit/chromium/public/WebDataSource.h" | |
21 #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" | |
22 #include "third_party/WebKit/Source/WebKit/chromium/public/WebRect.h" | |
23 #include "third_party/WebKit/Source/WebKit/chromium/public/WebSize.h" | |
24 #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" | |
25 #include "ui/gfx/color_utils.h" | |
26 #include "ui/gfx/skbitmap_operations.h" | |
27 #include "webkit/glue/webkit_glue.h" | |
28 | |
29 using WebKit::WebDataSource; | |
30 using WebKit::WebFrame; | |
31 using WebKit::WebRect; | |
32 using WebKit::WebSize; | |
33 using WebKit::WebView; | |
34 | |
35 // Delay in milliseconds that we'll wait before capturing the page contents | |
36 // and thumbnail. | |
37 static const int kDelayForCaptureMs = 500; | |
38 | |
39 // Typically, we capture the page data once the page is loaded. | |
40 // Sometimes, the page never finishes to load, preventing the page capture | |
41 // To workaround this problem, we always perform a capture after the following | |
42 // delay. | |
43 static const int kDelayForForcedCaptureMs = 6000; | |
44 | |
45 // define to write the time necessary for thumbnail/DOM text retrieval, | |
46 // respectively, into the system debug log | |
47 // #define TIME_TEXT_RETRIEVAL | |
48 | |
49 // maximum number of characters in the document to index, any text beyond this | |
50 // point will be clipped | |
51 static const size_t kMaxIndexChars = 65535; | |
52 | |
53 // Size of the thumbnails that we'll generate | |
54 static const int kThumbnailWidth = 212; | |
55 static const int kThumbnailHeight = 132; | |
56 | |
57 static bool PaintViewIntoCanvas(WebView* view, | |
58 skia::PlatformCanvas& canvas) { | |
59 view->layout(); | |
60 const WebSize& size = view->size(); | |
61 | |
62 if (!canvas.initialize(size.width, size.height, true)) | |
63 return false; | |
64 | |
65 view->paint(webkit_glue::ToWebCanvas(&canvas), | |
66 WebRect(0, 0, size.width, size.height)); | |
67 // TODO: Add a way to snapshot the whole page, not just the currently | |
68 // visible part. | |
69 | |
70 return true; | |
71 } | |
72 | |
73 // Calculates how "boring" a thumbnail is. The boring score is the | |
74 // 0,1 ranged percentage of pixels that are the most common | |
75 // luma. Higher boring scores indicate that a higher percentage of a | |
76 // bitmap are all the same brightness. | |
77 static double CalculateBoringScore(SkBitmap* bitmap) { | |
78 int histogram[256] = {0}; | |
79 color_utils::BuildLumaHistogram(bitmap, histogram); | |
80 | |
81 int color_count = *std::max_element(histogram, histogram + 256); | |
82 int pixel_count = bitmap->width() * bitmap->height(); | |
83 return static_cast<double>(color_count) / pixel_count; | |
84 } | |
85 | |
86 ChromeRenderObserver::ChromeRenderObserver( | |
87 RenderView* render_view, | |
88 TranslateHelper* translate_helper, | |
89 safe_browsing::PhishingClassifierDelegate* phishing_classifier) | |
90 : RenderViewObserver(render_view), | |
91 translate_helper_(translate_helper), | |
92 phishing_classifier_(phishing_classifier), | |
93 last_indexed_page_id_(-1), | |
94 ALLOW_THIS_IN_INITIALIZER_LIST(page_info_method_factory_(this)) { | |
95 } | |
96 | |
97 ChromeRenderObserver::~ChromeRenderObserver() { | |
98 } | |
99 | |
100 bool ChromeRenderObserver::OnMessageReceived(const IPC::Message& message) { | |
101 bool handled = true; | |
102 IPC_BEGIN_MESSAGE_MAP(ChromeRenderObserver, message) | |
103 IPC_MESSAGE_HANDLER(ViewMsg_CaptureSnapshot, OnCaptureSnapshot) | |
104 IPC_MESSAGE_UNHANDLED(handled = false) | |
105 IPC_END_MESSAGE_MAP() | |
106 return handled; | |
107 } | |
108 | |
109 void ChromeRenderObserver::OnCaptureSnapshot() { | |
110 SkBitmap snapshot; | |
111 bool error = false; | |
112 | |
113 WebFrame* main_frame = render_view()->webview()->mainFrame(); | |
114 if (!main_frame) | |
115 error = true; | |
116 | |
117 if (!error && !CaptureSnapshot(render_view()->webview(), &snapshot)) | |
118 error = true; | |
119 | |
120 DCHECK(error == snapshot.empty()) << | |
121 "Snapshot should be empty on error, non-empty otherwise."; | |
122 | |
123 // Send the snapshot to the browser process. | |
124 Send(new ViewHostMsg_Snapshot(routing_id(), snapshot)); | |
125 } | |
126 | |
127 void ChromeRenderObserver::DidStopLoading() { | |
128 MessageLoop::current()->PostDelayedTask( | |
129 FROM_HERE, | |
130 page_info_method_factory_.NewRunnableMethod( | |
131 &ChromeRenderObserver::CapturePageInfo, render_view()->page_id(), | |
132 false), | |
133 render_view()->content_state_immediately() ? 0 : kDelayForCaptureMs); | |
134 } | |
135 | |
136 void ChromeRenderObserver::DidCommitProvisionalLoad(WebFrame* frame, | |
137 bool is_new_navigation) { | |
138 if (!is_new_navigation) | |
139 return; | |
140 | |
141 MessageLoop::current()->PostDelayedTask( | |
142 FROM_HERE, | |
143 page_info_method_factory_.NewRunnableMethod( | |
144 &ChromeRenderObserver::CapturePageInfo, render_view()->page_id(), | |
145 true), | |
146 kDelayForForcedCaptureMs); | |
147 } | |
148 | |
149 void ChromeRenderObserver::CapturePageInfo(int load_id, | |
150 bool preliminary_capture) { | |
151 if (load_id != render_view()->page_id()) | |
152 return; // This capture call is no longer relevant due to navigation. | |
153 | |
154 if (load_id == last_indexed_page_id_) | |
155 return; // we already indexed this page | |
156 | |
157 if (!render_view()->webview()) | |
158 return; | |
159 | |
160 WebFrame* main_frame = render_view()->webview()->mainFrame(); | |
161 if (!main_frame) | |
162 return; | |
163 | |
164 // Don't index/capture pages that are in view source mode. | |
165 if (main_frame->isViewSourceModeEnabled()) | |
166 return; | |
167 | |
168 // Don't index/capture pages that failed to load. This only checks the top | |
169 // level frame so the thumbnail may contain a frame that failed to load. | |
170 WebDataSource* ds = main_frame->dataSource(); | |
171 if (ds && ds->hasUnreachableURL()) | |
172 return; | |
173 | |
174 if (!preliminary_capture) | |
175 last_indexed_page_id_ = load_id; | |
176 | |
177 // Get the URL for this page. | |
178 GURL url(main_frame->url()); | |
179 if (url.is_empty()) | |
180 return; | |
181 | |
182 // Retrieve the frame's full text. | |
183 string16 contents; | |
184 CaptureText(main_frame, &contents); | |
185 if (contents.size()) { | |
186 if (translate_helper_) | |
187 translate_helper_->PageCaptured(contents); | |
188 // Send the text to the browser for indexing (the browser might decide not | |
189 // to index, if the URL is HTTPS for instance) and language discovery. | |
190 Send(new ViewHostMsg_PageContents(routing_id(), url, load_id, contents)); | |
191 } | |
192 | |
193 // Generate the thumbnail here if the in-browser thumbnailing isn't | |
194 // enabled. TODO(satorux): Remove this and related code once | |
195 // crbug.com/65936 is complete. | |
196 if (!CommandLine::ForCurrentProcess()->HasSwitch( | |
197 switches::kEnableInBrowserThumbnailing)) { | |
198 CaptureThumbnail(); | |
199 } | |
200 | |
201 // Will swap out the string. | |
202 if (phishing_classifier_) | |
203 phishing_classifier_->PageCaptured(&contents, preliminary_capture); | |
204 } | |
205 | |
206 void ChromeRenderObserver::CaptureText(WebFrame* frame, string16* contents) { | |
207 contents->clear(); | |
208 if (!frame) | |
209 return; | |
210 | |
211 #ifdef TIME_TEXT_RETRIEVAL | |
212 double begin = time_util::GetHighResolutionTimeNow(); | |
213 #endif | |
214 | |
215 // get the contents of the frame | |
216 *contents = frame->contentAsText(kMaxIndexChars); | |
217 | |
218 #ifdef TIME_TEXT_RETRIEVAL | |
219 double end = time_util::GetHighResolutionTimeNow(); | |
220 char buf[128]; | |
221 sprintf_s(buf, "%d chars retrieved for indexing in %gms\n", | |
222 contents.size(), (end - begin)*1000); | |
223 OutputDebugStringA(buf); | |
224 #endif | |
225 | |
226 // When the contents are clipped to the maximum, we don't want to have a | |
227 // partial word indexed at the end that might have been clipped. Therefore, | |
228 // terminate the string at the last space to ensure no words are clipped. | |
229 if (contents->size() == kMaxIndexChars) { | |
230 size_t last_space_index = contents->find_last_of(kWhitespaceUTF16); | |
231 if (last_space_index == std::wstring::npos) | |
232 return; // don't index if we got a huge block of text with no spaces | |
233 contents->resize(last_space_index); | |
234 } | |
235 } | |
236 | |
237 void ChromeRenderObserver::CaptureThumbnail() { | |
238 WebFrame* main_frame = render_view()->webview()->mainFrame(); | |
239 if (!main_frame) | |
240 return; | |
241 | |
242 // get the URL for this page | |
243 GURL url(main_frame->url()); | |
244 if (url.is_empty()) | |
245 return; | |
246 | |
247 if (render_view()->size().IsEmpty()) | |
248 return; // Don't create an empty thumbnail! | |
249 | |
250 ThumbnailScore score; | |
251 SkBitmap thumbnail; | |
252 if (!CaptureFrameThumbnail(render_view()->webview(), kThumbnailWidth, | |
253 kThumbnailHeight, &thumbnail, &score)) | |
254 return; | |
255 | |
256 // send the thumbnail message to the browser process | |
257 Send(new ViewHostMsg_Thumbnail(routing_id(), url, score, thumbnail)); | |
258 } | |
259 | |
260 bool ChromeRenderObserver::CaptureFrameThumbnail(WebView* view, | |
261 int w, | |
262 int h, | |
263 SkBitmap* thumbnail, | |
264 ThumbnailScore* score) { | |
265 base::TimeTicks beginning_time = base::TimeTicks::Now(); | |
266 | |
267 skia::PlatformCanvas canvas; | |
268 | |
269 // Paint |view| into |canvas|. | |
270 if (!PaintViewIntoCanvas(view, canvas)) | |
271 return false; | |
272 | |
273 skia::BitmapPlatformDevice& device = | |
274 static_cast<skia::BitmapPlatformDevice&>(canvas.getTopPlatformDevice()); | |
275 | |
276 const SkBitmap& src_bmp = device.accessBitmap(false); | |
277 | |
278 SkRect dest_rect = { 0, 0, SkIntToScalar(w), SkIntToScalar(h) }; | |
279 float dest_aspect = dest_rect.width() / dest_rect.height(); | |
280 | |
281 // Get the src rect so that we can preserve the aspect ratio while filling | |
282 // the destination. | |
283 SkIRect src_rect; | |
284 if (src_bmp.width() < dest_rect.width() || | |
285 src_bmp.height() < dest_rect.height()) { | |
286 // Source image is smaller: we clip the part of source image within the | |
287 // dest rect, and then stretch it to fill the dest rect. We don't respect | |
288 // the aspect ratio in this case. | |
289 src_rect.set(0, 0, static_cast<S16CPU>(dest_rect.width()), | |
290 static_cast<S16CPU>(dest_rect.height())); | |
291 score->good_clipping = false; | |
292 } else { | |
293 float src_aspect = static_cast<float>(src_bmp.width()) / src_bmp.height(); | |
294 if (src_aspect > dest_aspect) { | |
295 // Wider than tall, clip horizontally: we center the smaller thumbnail in | |
296 // the wider screen. | |
297 S16CPU new_width = static_cast<S16CPU>(src_bmp.height() * dest_aspect); | |
298 S16CPU x_offset = (src_bmp.width() - new_width) / 2; | |
299 src_rect.set(x_offset, 0, new_width + x_offset, src_bmp.height()); | |
300 score->good_clipping = false; | |
301 } else { | |
302 src_rect.set(0, 0, src_bmp.width(), | |
303 static_cast<S16CPU>(src_bmp.width() / dest_aspect)); | |
304 score->good_clipping = true; | |
305 } | |
306 } | |
307 | |
308 score->at_top = (view->mainFrame()->scrollOffset().height == 0); | |
309 | |
310 SkBitmap subset; | |
311 device.accessBitmap(false).extractSubset(&subset, src_rect); | |
312 | |
313 // First do a fast downsample by powers of two to get close to the final size. | |
314 SkBitmap downsampled_subset = | |
315 SkBitmapOperations::DownsampleByTwoUntilSize(subset, w, h); | |
316 | |
317 // Do a high-quality resize from the downscaled size to the final size. | |
318 *thumbnail = skia::ImageOperations::Resize( | |
319 downsampled_subset, skia::ImageOperations::RESIZE_LANCZOS3, w, h); | |
320 | |
321 score->boring_score = CalculateBoringScore(thumbnail); | |
322 | |
323 HISTOGRAM_TIMES("Renderer4.Thumbnail", | |
324 base::TimeTicks::Now() - beginning_time); | |
325 | |
326 return true; | |
327 } | |
328 | |
329 bool ChromeRenderObserver::CaptureSnapshot(WebView* view, SkBitmap* snapshot) { | |
330 base::TimeTicks beginning_time = base::TimeTicks::Now(); | |
331 | |
332 skia::PlatformCanvas canvas; | |
333 if (!PaintViewIntoCanvas(view, canvas)) | |
334 return false; | |
335 | |
336 skia::BitmapPlatformDevice& device = | |
337 static_cast<skia::BitmapPlatformDevice&>(canvas.getTopPlatformDevice()); | |
338 | |
339 const SkBitmap& bitmap = device.accessBitmap(false); | |
340 if (!bitmap.copyTo(snapshot, SkBitmap::kARGB_8888_Config)) | |
341 return false; | |
342 | |
343 HISTOGRAM_TIMES("Renderer4.Snapshot", | |
344 base::TimeTicks::Now() - beginning_time); | |
345 return true; | |
346 } | |
OLD | NEW |